28 Commits
v2.4 ... v3.1

Author SHA1 Message Date
9bb8e8d3d7 Merge pull request 'Python 3.11 - Alpine + loop improvement' (#11) from wip into main
Reviewed-on: #11
2025-07-09 18:14:14 +02:00
66c4146223 Python 3.11 - Alpine + loop improvement 2025-07-09 16:13:12 +00:00
92edccd8d8 Merge pull request 'Multi GPU merge' (#10) from wip into main
Reviewed-on: #10
2025-07-08 19:15:29 +02:00
a2561a79b8 Fixed README 2025-07-08 16:58:26 +00:00
d4735e32b3 Fixed README 2025-07-08 16:57:49 +00:00
28991dd7a7 Fixed summary 2025-07-08 16:34:49 +00:00
781fb1270a Fixed summary 2025-07-08 16:32:29 +00:00
0eb569f7ec Updated README 2025-07-08 16:28:14 +00:00
be263e8cd1 One role per GPU support 2025-07-08 15:54:56 +00:00
6c4be66f3d Multi GPU support 2025-07-08 14:34:55 +00:00
7ca0483cfd Fixed readme 2025-07-08 09:34:06 +00:00
c8dcafbe01 v2.6 - new compose + new python execution process 2025-07-08 09:29:22 +00:00
1f9a237fce Added role env var 2025-07-08 09:15:56 +00:00
7280146d5e Removed french discord link 2025-07-07 14:12:58 +00:00
f86c56b7d1 Merge branch 'wip' 2025-07-06 13:12:35 +00:00
afbc79bee4 Github warn 2025-07-06 13:08:20 +00:00
9c2825736e Merge pull request 'wip - In english please' (#9) from wip into main
Reviewed-on: #9
2025-06-03 15:21:36 +02:00
bfd8428073 Fixed logo 2025-06-03 13:19:02 +00:00
b0eef8b8df Logo translated 2025-06-03 13:17:10 +00:00
e2d86cb787 Fully translated 2025-06-03 12:58:34 +00:00
8124ad6a2d In english please 2025-06-03 12:41:49 +00:00
27966c04e5 Merge pull request 'Correction structure, orthographe, emoji' (#8) from wip into main
Reviewed-on: #8
2025-04-24 12:53:48 +02:00
17ade38bcd Correction structure, orthographe, emoji 2025-04-24 10:51:52 +00:00
2f8ae16533 Merge pull request 'Correction variables par défaut' (#7) from wip into main
Reviewed-on: #7
2025-04-23 21:12:32 +02:00
80fe61f91d Minor changes 2025-04-23 18:58:15 +00:00
f07ff328e5 Merge pull request 'wip' (#6) from wip into main
Reviewed-on: #6
2025-04-23 19:00:54 +02:00
6657c03b07 Minor changes 2025-04-23 16:59:04 +00:00
9dae286965 Mise à jour de la documentation 2025-04-23 16:57:35 +00:00
8 changed files with 300 additions and 259 deletions

View File

@ -1,22 +1,16 @@
# Utiliser une image de base légère de Python (ici Python 3.9) FROM python:3.11-alpine
FROM python:3.9-slim
RUN apk add --no-cache ca-certificates
# Définir le répertoire de travail à l'intérieur du conteneur
WORKDIR /app WORKDIR /app
# Copier le script Python dans le répertoire de travail
COPY nvidia-stock-bot.py /app/ COPY nvidia-stock-bot.py /app/
# Copier un éventuel fichier requirements.txt pour installer des dépendances
# Si des dépendances supplémentaires sont nécessaires, ajoutez-les dans requirements.txt
COPY requirements.txt /app/ COPY requirements.txt /app/
# Installer les dépendances Python à partir de requirements.txt
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Définir les variables d'environnement par défaut (modifiable lors du lancement du conteneur)
ENV DISCORD_WEBHOOK_URL="https://example.com/webhook" \ ENV DISCORD_WEBHOOK_URL="https://example.com/webhook" \
REFRESH_TIME="60" REFRESH_TIME="30"
# Exposer un point de commande pour exécuter le script
CMD ["python", "nvidia-stock-bot.py"] CMD ["python", "nvidia-stock-bot.py"]

186
README.md
View File

@ -1,165 +1,185 @@
<h1 align="center"> Nvidia Stock Bot</h1> <h1 align="center">Nvidia Stock Bot</h1>
<div align="center"> <div align="center">
<a href="https://discord.gg/gxffg3GA96">
<img src="https://img.shields.io/badge/JV%20hardware-rejoindre-green?style=flat-square&logo=discord&logoColor=%23fff" alt="JV Hardware">
<a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank"> <a href="https://creativecommons.org/licenses/by-nc/4.0/" target="_blank">
<img src="https://img.shields.io/badge/License-CC%20BY--NC%204.0-8E44AD?style=flat-square" alt="License: CC BY-NC 4.0"> <img src="https://img.shields.io/badge/License-CC%20BY--NC%204.0-8E44AD?style=flat-square" alt="License: CC BY-NC 4.0">
</a> </a>
</div> </div>
<div align="center" > <div align="center" >
<img src="https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/nvidia-stock-bot-logo.png" alt="Nvidia Stock Bot" width="300"> <img src="https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/nvidia-stock-bot-logo.png" alt="Nvidia Stock Bot" width="300">
</div> </div>
**Nvidia Stock Bot** - Un robot qui permet d'être alerté en temps réel des stocks de cartes graphiques **Nvidia RTX FE** grâce à des notifications Discord. **🤖 Nvidia Stock Bot** - A bot that alerts you in real-time about **Nvidia RTX FE** GPU stock availability through Discord notifications.
*Le code a été en partie rédigé et structuré à l'aide d'une IA générative.* > [!NOTE]
>_The code was partially written and structured using a generative AI._
>
>_Github repo is a mirror of https://git.djeex.fr/Djeex/nvidia-stock-bot. You'll find full package, history and release note there._
## Sommaire ## 📌 Table of Contents
- [Fonctionnalités](#fonctionnalit%C3%A9s) - [✨ Features](#-features)
- [Installation docker sans le dépot (rapide)](#installation-sans-le-d%C3%A9pot-avec-docker-compose) - [🐳 Docker Installation without cloning the repo (quick)](#-docker-installation-without-the-repo-quick)
- [Installation docker avec le dépot (développeur)](#installation-avec-le-d%C3%A9pot) - [🐙 Docker Installation with the repo (developer)](#-docker-installation-with-the-repo)
- [Installation avec Python (développeur)](#installation-avec-python) - [🐍 Python Installation (developer)](#-python-installation)
- [Captures d'écran](#captures-d%C3%A9cran) - [🖼️ Screenshots](#-screenshots)
- [Contributeurs](#contributeurs) - [🐞 Common issues](#-common-issues)
- [🧑‍💻 Contributors](#-contributors)
## Fonctionnalités ## ✨ Features
- Notification Discord `@everyone` en cas de changement du SKU (potentiel drop imminent) - Discord `@everyone` notification on SKU change (possible imminent drop)
- Notification Discord `@everyone` en cas de stock détecté avec modèle, prix, et lien - Discord `@everyone` notification when stock is detected, including model, price, and link
- Notification Discord silencieuse en cas d'absence de stock détécté - Silent Discord notification when no stock is detected
- Choix de la fréquence de la vérification - Selectable check frequency
- Choix du modèle à surveiller - Selectable GPU model
<img src="https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/nvbot_schematics.png" align="center"> ## 🐳 Docker Installation without the repo (quick)
## Installation sans le dépot avec docker compose Below are the instructions to set up the container using our pre-built image. With this setup, your bot will run independently as long as the container is active.
Vous trouverez-ci dessous les instructions pour configurer le conteneur avec notre image pré-compilée. Avec cette solution, votre bot tournera tout seul tant que le conteneur est actif. **Requirements**
**Pré-requis**
- [Docker](https://docs.docker.com/engine/install/) - [Docker](https://docs.docker.com/engine/install/)
**Configuration** **Configuration**
- Créez un dossier `nvidia-stock-bot` - Create a folder named `nvidia-stock-bot`
- Créez le fichier `compose.yaml` dans ce dossier avec la configuration ci-dessous : - Create a `compose.yaml` file inside that folder with the following content:
```yaml ```yaml
version: "3.8"
services: services:
nvidia-stock-bot: nvidia-stock-bot:
image: git.djeex.fr/djeex/nvidia-stock-bot:latest image: git.djeex.fr/djeex/nvidia-stock-bot:latest
container_name: nvidia-stock-bot container_name: nvidia-stock-bot
restart: always restart: unless-stopped
environment: environment:
- DISCORD_WEBHOOK_URL= # URL de votre webhook Discord - PRODUCT_NAMES= # Exact GPU name (e.g. "RTX 5080, RTX 5090")
- REFRESH_TIME= # Durée de rafraichissement du script en secondes - DISCORD_ROLES= # List of Discord roles ID (e.g. "<@&12345>, <@&6789>"), in the same order than PRODUCT_NAMES values. @everyone by default.
- API_URL_SKU= # API listant le produit par exemple https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia&gpu=RTX%205090 - DISCORD_WEBHOOK_URL= # Your Discord webhook URL
- API_URL_STOCK= # API appelant le stock sans préciser la valeur du sku, par exemple https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus= - API_URL_SKU= # API listing the product
- PRODUCT_URL= # URL d'achat du GPU - API_URL_STOCK= # API providing stock data
- PRODUCT_NAME= # Le nom du GPU qui s'affiche dans les notifications - PRODUCT_URL= # GPU purchase URL
- TEST_MODE= # true pour tester les notifications discord. false par défaut. - PYTHONUNBUFFERED=1 # Enables real-time log output
- PYTHONUNBUFFERED=1 # Permet d'afficher les logs en temps réel command: python nvidia-stock-bot.py
command: python nvidia-stock-bot.py # Lance le script Python au démarrage du conteneur
``` ```
**Lancer l'image** **Environment Variables:**
Rendez-vous dans le dossier `nvidia-stock-bot` et lancez le conteneur : | Variable | Description | Possible Values | Default Value |
|---------------------|-------------------------------------------------|------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------|
| `DISCORD_WEBHOOK_URL` | Your Discord webhook URL | A valid URL | |
| `PRODUCT_NAMES` | The exact GPU names you're searching for | `RTX 5080, RTX 5090` | |
| `DISCORD_ROLES` | List of Discord roles ID in the same order than `PRODUCT_NAMES` values, found in your discord server settings (with user profile developer mode enabled) | `<@&12345><@&6789>` | @everyone |
| `REFRESH_TIME` | Script refresh interval in seconds | `60`, `30`, etc. | `30` |
| `API_URL_SKU` | API listing the product | A URL. API url can change over time. For now, you can use the default one and change the `locale` parameter to yours (e.g. `locale=en-gb`) | `https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia` |
| `API_URL_STOCK` | API providing stock data | A URL. API url can change over time. For now, you can use the default one and change the `locale` parameter to yours (e.g. `locale=en-gb`) | `https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus=` |
| `PRODUCT_URL` | GPU purchase URL. There isn't any direct link workinf right now, so put the generic marketplace url listing all FE products | A URL. API url can change over time. For now, you can use the default one and change the locale parameter to yours (e.g. `/en-gb/`) | `https://marketplace.nvidia.com/fr-fr/consumer/graphics-cards/?locale=fr-fr&page=1&limit=12&manufacturer=NVIDIA` |
| `TEST_MODE` | For testing without sending notifications | `True`, `False` | `False` |
| `PYTHONUNBUFFERED` | Enables real-time log output | `1`, `0` | `1` |
**Run the image**
Navigate to the `nvidia-stock-bot` folder and launch the container:
```sh ```sh
docker compose up -d docker compose up -d
``` ```
**Voir les logs pour vérifier le bon fonctionnement** **Check logs to verify operation**
```sh ```sh
docker logs -f nvidia-stock-bot docker logs -f nvidia-stock-bot
``` ```
## Installation avec le dépot ## 📦 Docker Installation with the repo
Vous trouverez-ci dessous les instructions pour installer le dépot, compiler l'image docker, et lancer le conteneur. Avec cette solution, votre bot tournera tout seul tant que le conteneur est actif. Instructions below show how to install the repo, build the Docker image, and launch the container. Your bot will run independently as long as the container is active.
**Pré-requis** **Requirements**
- [Git](https://git-scm.com/docs) - [Git](https://git-scm.com/docs)
- [Docker](https://docs.docker.com/engine/install/) - [Docker](https://docs.docker.com/engine/install/)
**Cloner et paramétrer** **Clone and configure**
- Clonez le repo : - Clone the repo:
```sh ```sh
git clone https://git.djeex.fr/Djeex/nvidia-stock-bot.git git clone https://git.djeex.fr/Djeex/nvidia-stock-bot.git
``` ```
- Rendez vous dans le dossier `nvidia-stock-bot` et compilez l'image docker : - Navigate to `nvidia-stock-bot` and build the Docker image:
```sh ```sh
docker build -t nvidia-stock-bot . docker build -t nvidia-stock-bot .
``` ```
- Puis rendez-vous dans le dossier `nvidia-stock-bot/docker` et éditez le fichier `.env` avec : - Then go to `nvidia-stock-bot/docker` and edit the `.env` file with:
- l'url de votre webhook discord - Your Discord webhook URL
- les différents liens API et produits - The API and product URLs
- la fréquence de consultation des stock (par défaut 60s, attention à ne pas trop descendre sous peine de blocage de votre adresse IP par nVidia) - Stock checking frequency (default: 60s; lowering too much may get your IP blocked by Nvidia)
**Lancer l'image** **Run the image**
Rendez-vous dans le dossier `nvidia-stock-bot/docker` et lancez le conteneur : Navigate to `nvidia-stock-bot/docker` and launch the container:
```sh ```sh
docker compose up -d docker compose up -d
``` ```
**Voir les logs pour vérifier le bon fonctionnement** **Check logs to verify operation**
```sh ```sh
docker logs -f nvidia-stock-bot docker logs -f nvidia-stock-bot
``` ```
## Installation avec Python ## 🐍 Python Installation
Vous trouverez ci-dessous comment exécuter directement le script Python. Avec cette solution, le bot s'arretera si vous fermez votre terminal. Instructions to directly run the Python script. Note: the bot stops when you close the terminal.
**Pré-requis** **Requirements**
- Python 3.11 ou plus - Python 3.11 or newer
- requests : `pip install requests` - requests: `pip install requests`
**Configuration** **Configuration**
- Créez un environnement virtuel (exemple : `python3 -m venv nom_de_l_environnement` ) - Clone the repo:
- Créez un dossier et aller dedans
- Téléchargez le script python :
```sh ```sh
curl -o nvidia-stock-bot.py -# https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/nvidia-stock-bot.py git clone https://git.djeex.fr/Djeex/nvidia-stock-bot.git
``` ```
- Exportez les variables d'environnement avec votre webhook discord et le temps de rafraichissement en secondes, par exemple : - Navigate to `nvidia-stock-bot` and create a virtual environment (e.g., `python3 -m venv env_name`)
- Export the environment variables with your webhook and refresh time, for exemple:
```sh ```sh
export DISCORD_WEBHOOK_URL="https://votre_url_discord" export DISCORD_WEBHOOK_URL="https://your_discord_url"
export REFRESH_TIME="60" export PRODUCT_NAMES=RTX 5080, RTX 5090
export API_URL_SKU="https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia&gpu=RTX%205080" export DISCORD_ROLES=<@&12345>, <@&6789>
export API_URL_STOCK="https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus=" export REFRESH_TIME="60"
export PRODUCT_URL= "https://marketplace.nvidia.com/fr-fr/consumer/graphics-cards/?locale=fr-fr&page=1&limit=12&gpu=RTX%205080&manufacturer=NVIDIA" export API_URL_SKU="https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia&gpu=RTX%205080"
export PRODUCT_NAME="RTX 5080" export API_URL_STOCK="https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus="
export TEST_MODE=false export PRODUCT_URL="https://marketplace.nvidia.com/fr-fr/consumer/graphics-cards/?locale=fr-fr&page=1&limit=12&gpu=RTX%205080&manufacturer=NVIDIA"
``` export TEST_MODE=false
- Lancez le script export PYTHONUNBUFFERED=1
```
```sh - Run the script
python nvidia-stock-bot.py
```
## Captures d'écran ```sh
python nvidia-stock-bot.py
```
<div align="center" > ## 🖼️ Screenshots
<img src="https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/nvidia-stock-bot-discord.png" alt="Nvidia Stock Bot - captures">
<div align="center" >
<img src="https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/nvidia-stock-bot-discord.png" alt="Nvidia Stock Bot - screenshots">
</div> </div>
## Contributeurs ## 🐞 Common issues
On remercie pour leurs contributions : Error when trying to reach product API url :
- `API_SKU_URL` may be wrong
- Your IP may be blacklisted by nvidia. Try to use a VPN.
- nvidia API may be down
## 🧑‍💻 Contributors
Thanks for their contributions:
- Djeex - Djeex
- KevOut - KevOut

Binary file not shown.

Before

Width:  |  Height:  |  Size: 499 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,6 +1,6 @@
DS_HOOK= #votre url du webhook Discord DS_HOOK= # Your Discord webhook URL
FREQ= #frequence de rafraichissement en secondes FREQ= # Refresh frequency in seconds, default is 30
API_URL_SKU= # API listant le produit par exemple https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia&gpu=RTX%205090 API_URL_SKU= # API listing the product, default is https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia
API_URL_STOCK= # API appelant le stock sans préciser la valeur du sku, par exemple https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus= API_URL_STOCK= # API used to check stock without specifying the SKU, default is https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus=
PRODUCT_URL= # URL d'achat du GPU PRODUCT_URL= # Purchase URL of the GPU
PRODUCT_NAME= #Le nom du GPU qui s'affiche dans les notifications PRODUCT_NAME= # The exact name of the GPU you're looking for, e.g., "RTX 5080"

View File

@ -1,9 +1,8 @@
version: "3.8"
services: services:
nvidia-stock-bot: nvidia-stock-bot:
image: nvidia-stock-bot image: nvidia-stock-bot
container_name: nvidia-stock-bot container_name: nvidia-stock-bot
restart: always # Le conteneur redémarrera automatiquement en cas d'échec restart: unless-stopped
env_file: env_file:
- .env - .env
environment: environment:
@ -11,5 +10,7 @@ services:
- REFRESH_TIME=${FREQ} - REFRESH_TIME=${FREQ}
- API_URL_SKU=${API_URL_SKU} - API_URL_SKU=${API_URL_SKU}
- API_URL_STOCK=${API_URL_STOCK} - API_URL_STOCK=${API_URL_STOCK}
- PYTHONUNBUFFERED=1 # Permet d'afficher les logs en temps réel - PRODUCT_URL=${PRODUCT_URL}
command: python nvidia-stock-bot.py # Lance le script Python - PRODUCT_NAME=${PRODUCT_NAME}
- PYTHONUNBUFFERED=1 # Allow to display log in real-time
command: python nvidia-stock-bot.py # Run the script

View File

@ -5,56 +5,87 @@ import os
import re import re
from requests.adapters import HTTPAdapter, Retry from requests.adapters import HTTPAdapter, Retry
# Configuration du logger # Logger configuration
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s", format="%(asctime)s [%(levelname)s] %(message)s",
) )
logging.info("Démarrage du script") logging.info("Script started")
# Récupération des variables d'environnement # Retrieve environment variables
try: try:
DISCORD_WEBHOOK_URL = os.environ['DISCORD_WEBHOOK_URL'] DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL')
API_URL_SKU = os.environ['API_URL_SKU'] API_URL_SKU = os.environ.get('API_URL_SKU', 'https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia')
API_URL_STOCK = os.environ['API_URL_STOCK'] API_URL_STOCK = os.environ.get('API_URL_STOCK', 'https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus=')
REFRESH_TIME = int(os.environ['REFRESH_TIME']) # Convertir en entier REFRESH_TIME = int(os.environ.get('REFRESH_TIME'))
TEST_MODE = os.environ.get('TEST_MODE', 'False').lower() == 'true' TEST_MODE = os.environ.get('TEST_MODE', 'False').lower() == 'true'
PRODUCT_URL = os.environ['PRODUCT_URL'] PRODUCT_URL = os.environ.get('PRODUCT_URL', 'https://marketplace.nvidia.com/fr-fr/consumer/graphics-cards/?locale=fr-fr&page=1&limit=12&manufacturer=NVIDIA')
PRODUCT_NAME = os.environ['PRODUCT_NAME'] DISCORD_ROLES = os.environ.get('DISCORD_ROLES')
PRODUCT_NAMES = os.environ.get('PRODUCT_NAMES')
# Regex pour extraire l'ID et le token if not PRODUCT_NAMES:
logging.error("❌ PRODUCT_NAMES is required but not defined.")
exit(1)
PRODUCT_NAMES = [name.strip() for name in PRODUCT_NAMES.split(',')]
DISCORD_ROLE_MAP = {}
if not DISCORD_ROLES or not DISCORD_ROLES.strip():
logging.warning("⚠️ DISCORD_ROLES not defined or empty. Defaulting all roles to @everyone.")
for name in PRODUCT_NAMES:
DISCORD_ROLE_MAP[name] = '@everyone'
else:
roles = [r.strip() if r.strip() else '@everyone' for r in DISCORD_ROLES.split(',')]
if len(roles) != len(PRODUCT_NAMES):
logging.error("❌ The number of DISCORD_ROLES must match PRODUCT_NAMES.")
exit(1)
for name, role in zip(PRODUCT_NAMES, roles):
if role != '@everyone' and not re.match(r'^<@&\d{17,20}>$', role):
logging.error(f"❌ Invalid DISCORD_ROLE format for {name}: {role}")
exit(1)
DISCORD_ROLE_MAP[name] = role
if not DISCORD_WEBHOOK_URL:
logging.error("❌ DISCORD_WEBHOOK_URL is required but not defined.")
exit(1)
# Regex to extract ID and token
match = re.search(r'/(\d+)/(.*)', DISCORD_WEBHOOK_URL) match = re.search(r'/(\d+)/(.*)', DISCORD_WEBHOOK_URL)
if match: if match:
webhook_id = match.group(1) webhook_id = match.group(1)
webhook_token = match.group(2) webhook_token = match.group(2)
# Masquer derniers caractères de l'ID # Mask last characters of the ID
masked_webhook_id = webhook_id[:len(webhook_id) - 10] + '*' * 10 masked_webhook_id = webhook_id[:len(webhook_id) - 10] + '*' * 10
# Masquer derniers caractères du token # Mask last characters of the token
masked_webhook_token = webhook_token[:len(webhook_token) - 120] + '*' * 10 masked_webhook_token = webhook_token[:len(webhook_token) - 120] + '*' * 10
# Reconstruction de l'url masquée # Rebuild masked URL
wh_masked_url = f"https://discord.com/api/webhooks/{masked_webhook_id}/{masked_webhook_token}" wh_masked_url = f"https://discord.com/api/webhooks/{masked_webhook_id}/{masked_webhook_token}"
# Error logging
except KeyError as e: except KeyError as e:
logging.error(f"Variable d'environnement manquante : {e}") logging.error(f"Missing environment variable: {e}")
exit(1) exit(1)
except ValueError: except ValueError:
logging.error("REFRESH_TIME doit être un entier valide.") logging.error("REFRESH_TIME must be a valid integer.")
exit(1) exit(1)
# Affichage des URLs et configurations # Display URLs and configurations
logging.info(f"GPU: {PRODUCT_NAME}") logging.info(f"GPU: {PRODUCT_NAMES}")
logging.info(f"URL Webhook Discord: {wh_masked_url}") logging.info(f"Discord Webhook URL: {wh_masked_url}")
logging.info(f"URL API SKU: {API_URL_SKU}") logging.info(f"Discord Role Mention: {DISCORD_ROLES}")
logging.info(f"URL API Stock: {API_URL_STOCK}") logging.info(f"API URL SKU: {API_URL_SKU}")
logging.info(f"URL produit: {PRODUCT_URL}") logging.info(f"API URL Stock: {API_URL_STOCK}")
logging.info(f"Temps d'actualisation: {REFRESH_TIME} secondes") logging.info(f"Product URL: {PRODUCT_URL}")
logging.info(f"Mode Test: {TEST_MODE}") logging.info(f"Refresh time: {REFRESH_TIME} seconds")
logging.info(f"Test Mode: {TEST_MODE}")
# HTTP headers
# Entêtes HTTP
HEADERS = { HEADERS = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) " "AppleWebKit/537.36 (KHTML, like Gecko) "
@ -73,30 +104,27 @@ HEADERS = {
"Sec-GPC": "1", "Sec-GPC": "1",
} }
# Session avec retries # Session with retries
session = requests.Session() session = requests.Session()
retries = Retry(total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504]) retries = Retry(total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
session.mount('https://', HTTPAdapter(max_retries=retries)) session.mount('https://', HTTPAdapter(max_retries=retries))
# Stockage de l'état des stocks last_sku_dict = {}
global_stock_status = {} global_stock_status_dict = {}
first_run_dict = {name: True for name in PRODUCT_NAMES}
# Stocke le dernier SKU connu # Discord notifications
last_sku = None
first_run = True # Before calling check_rtx_50_founders
# Notifications Discord
def send_discord_notification(gpu_name: str, product_link: str, products_price: str): def send_discord_notification(gpu_name: str, product_link: str, products_price: str):
# Récupérer le timestamp UNIX actuel # Get current UNIX timestamp
timestamp_unix = int(time.time()) timestamp_unix = int(time.time())
if TEST_MODE: if TEST_MODE:
logging.info(f"[TEST MODE] Notification Discord: {gpu_name} disponible !") logging.info(f"[TEST MODE] Discord Notification: {gpu_name} available!")
return return
embed = { embed = {
"title": f"🚀 {PRODUCT_NAME} EN STOCK !", "title": f"🚀 {gpu_name} IN STOCK!",
"color": 3066993, "color": 3066993,
"thumbnail": { "thumbnail": {
"url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/RTX5000.jpg" "url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/RTX5000.jpg"
@ -107,45 +135,52 @@ def send_discord_notification(gpu_name: str, product_link: str, products_price:
"fields": [ "fields": [
{ {
"name": "Prix", "name": "Price",
"value": f"`{products_price}€`", "value": f"`{products_price}€`",
"inline": True "inline": True
}, },
{ {
"name": "Heure", "name": "Time",
"value": f"<t:{timestamp_unix}:d> <t:{timestamp_unix}:T>", "value": f"<t:{timestamp_unix}:d> <t:{timestamp_unix}:T>",
"inline": True "inline": True
}, },
], ],
"description": f"**:point_right: [Acheter maintenant]({product_link})**", "description": f"**:point_right: [Buy now]({product_link})**",
"footer": { "footer": {
"text": "NviBot • JV Hardware 2.0", "text": "NviBot • JV Hardware 2.0",
"icon_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg" "icon_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg"
} }
} }
payload = {"content": "@everyone", "username": "NviBot", "avatar_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg", "embeds": [embed]}
payload = {
"content": f"{DISCORD_ROLE_MAP.get(gpu_name, '@everyone')}",
"username": "NviBot",
"avatar_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg",
"embeds": [embed]
}
try: try:
response = requests.post(DISCORD_WEBHOOK_URL, json=payload) response = requests.post(DISCORD_WEBHOOK_URL, json=payload)
if response.status_code == 204: if response.status_code == 204:
logging.info("✅ Notification envoyée sur Discord.") logging.info("✅ Notification sent to Discord.")
else: else:
logging.error(f" Erreur Webhook : {response.status_code} - {response.text}") logging.error(f"❌ Webhook error: {response.status_code} - {response.text}")
except Exception as e: except Exception as e:
logging.error(f"🚨 Erreur lors de l'envoi du webhook : {e}") logging.error(f"🚨 Error sending webhook: {e}")
def send_out_of_stock_notification(gpu_name: str, product_link: str, products_price: str): def send_out_of_stock_notification(gpu_name: str, product_link: str, products_price: str):
# Récupérer le timestamp UNIX actuel # Get current UNIX timestamp
timestamp_unix = int(time.time()) timestamp_unix = int(time.time())
if TEST_MODE: if TEST_MODE:
logging.info(f"[TEST MODE] Notification Discord: {gpu_name} hors stock !") logging.info(f"[TEST MODE] Discord Notification: {gpu_name} out of stock!")
return return
embed = { embed = {
"title": f"{PRODUCT_NAME} n'est plus en stock", "title": f"{gpu_name} is out of stock",
"color": 15158332, # Rouge pour hors stock "color": 15158332, # Red for out of stock
"thumbnail": { "thumbnail": {
"url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/RTX5000.jpg" "url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/RTX5000.jpg"
}, },
@ -161,7 +196,7 @@ def send_out_of_stock_notification(gpu_name: str, product_link: str, products_pr
"fields": [ "fields": [
{ {
"name": "Heure", "name": "Time",
"value": f"<t:{timestamp_unix}:d> <t:{timestamp_unix}:T>", "value": f"<t:{timestamp_unix}:d> <t:{timestamp_unix}:T>",
"inline": True "inline": True
} }
@ -171,26 +206,26 @@ def send_out_of_stock_notification(gpu_name: str, product_link: str, products_pr
try: try:
response = requests.post(DISCORD_WEBHOOK_URL, json=payload) response = requests.post(DISCORD_WEBHOOK_URL, json=payload)
if response.status_code == 204: if response.status_code == 204:
logging.info("Notification 'hors stock' envoyée sur Discord.") logging.info("'Out of stock' notification sent to Discord.")
else: else:
logging.error(f" Erreur Webhook : {response.status_code} - {response.text}") logging.error(f"❌ Webhook error: {response.status_code} - {response.text}")
except Exception as e: except Exception as e:
logging.error(f"🚨 Erreur lors de l'envoi du webhook : {e}") logging.error(f"🚨 Error sending webhook: {e}")
def send_sku_change_notification(old_sku: str, new_sku: str, product_link: str): def send_sku_change_notification(gpu_name: str, old_sku: str, new_sku: str, product_link: str):
# Récupérer le timestamp UNIX actuel # Get current UNIX timestamp
timestamp_unix = int(time.time()) timestamp_unix = int(time.time())
if TEST_MODE: if TEST_MODE:
logging.info(f"[TEST MODE] Changement de SKU détecté : {old_sku}{new_sku}") logging.info(f"[TEST MODE] SKU change detected: {old_sku}{new_sku}")
return return
embed = { embed = {
"title": f"🔄 {PRODUCT_NAME} Changement de SKU détecté", "title": f"🔄 {gpu_name} SKU change detected",
"url": f"{product_link}", "url": f"{product_link}",
"description": f"**Ancien SKU** : `{old_sku}`\n**Nouveau SKU** : `{new_sku}`", "description": f"**Old SKU** : `{old_sku}`\n**New SKU** : `{new_sku}`",
"color": 16776960, # Jaune "color": 16776960, # Yellow
"footer": { "footer": {
"text": "NviBot • JV Hardware 2.0", "text": "NviBot • JV Hardware 2.0",
@ -199,7 +234,7 @@ def send_sku_change_notification(old_sku: str, new_sku: str, product_link: str):
"fields": [ "fields": [
{ {
"name": "Heure", "name": "Time",
"value": f"<t:{timestamp_unix}:d> <t:{timestamp_unix}:T>", "value": f"<t:{timestamp_unix}:d> <t:{timestamp_unix}:T>",
"inline": True "inline": True
} }
@ -207,7 +242,7 @@ def send_sku_change_notification(old_sku: str, new_sku: str, product_link: str):
} }
payload = { payload = {
"content": "@everyone ⚠️ Potentiel drop imminent !", "content": f"{DISCORD_ROLE_MAP.get(gpu_name, '@everyone')} ⚠️ Possible imminent drop!",
"username": "NviBot", "username": "NviBot",
"avatar_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg", "avatar_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg",
"embeds": [embed] "embeds": [embed]
@ -216,120 +251,111 @@ def send_sku_change_notification(old_sku: str, new_sku: str, product_link: str):
try: try:
response = requests.post(DISCORD_WEBHOOK_URL, json=payload) response = requests.post(DISCORD_WEBHOOK_URL, json=payload)
if response.status_code == 204: if response.status_code == 204:
logging.info("Notification de changement de SKU envoyée sur Discord.") logging.info("SKU change notification sent to Discord.")
else: else:
logging.error(f" Erreur Webhook : {response.status_code} - {response.text}") logging.error(f"❌ Webhook error: {response.status_code} - {response.text}")
except Exception as e: except Exception as e:
logging.error(f"🚨 Erreur lors de l'envoi du webhook : {e}") logging.error(f"🚨 Error sending webhook: {e}")
# Recherche du stock # Stock search
def check_rtx_50_founders(): def check_rtx_50_founders():
global global_stock_status, last_sku, first_run global last_sku_dict, global_stock_status_dict, first_run_dict
# Appel vers l'API produit pour récupérer le sku et l'upc
try: try:
response = session.get(API_URL_SKU, headers=HEADERS, timeout=10) response = session.get(API_URL_SKU, headers=HEADERS, timeout=10)
logging.info(f"Réponse de l'API SKU : {response.status_code}") logging.info(f"SKU API response: {response.status_code}")
response.raise_for_status() response.raise_for_status()
data = response.json() data = response.json()
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logging.error(f"Erreur API SKU : {e}") logging.error(f"SKU API error: {e}")
return return
# Recherche du produit dont le GPU correspond à PRODUCT_NAME # All available products
all_products = data['searchedProducts']['productDetails']
for product_name in PRODUCT_NAMES:
product_details = None product_details = None
for p in all_products:
for p in data['searchedProducts']['productDetails']: if p.get("gpu", "").strip() == product_name:
gpu_name = p.get("gpu", "").strip()
# Si le GPU correspond exactement à PRODUCT_NAME
if gpu_name == PRODUCT_NAME.strip():
product_details = p product_details = p
break # Sortir dès qu'on trouve le bon produit break
if not product_details: if not product_details:
logging.warning(f"⚠️ Aucun produit avec le GPU '{PRODUCT_NAME}' trouvé.") logging.warning(f"⚠️ No product with GPU '{product_name}' found.")
return continue
# Récupérer le SKU pour le GPU trouvé
product_sku = product_details['productSKU'] product_sku = product_details['productSKU']
product_upc = product_details.get('productUPC', "") product_upc = product_details.get('productUPC', "")
# Vérifier si c'est la première exécution
if last_sku is not None and product_sku != last_sku:
if not first_run: # Évite d'envoyer une notification au premier appel
product_link = PRODUCT_URL
logging.warning(f"⚠️ SKU modifié : {last_sku}{product_sku}")
send_sku_change_notification(last_sku, product_sku, product_link)
# Mettre à jour le SKU stocké
last_sku = product_sku
first_run = False # Désactive la protection après la première exécution
if not isinstance(product_upc, list): if not isinstance(product_upc, list):
product_upc = [product_upc] product_upc = [product_upc]
# Construction de l'url de l'API de stock et appel pour vérifier le statut # Check SKU change
API_URL = API_URL_STOCK + product_sku old_sku = last_sku_dict.get(product_name)
logging.info(f"URL de l'API de stock appelée : {API_URL}") if old_sku and old_sku != product_sku and not first_run_dict[product_name]:
logging.warning(f"⚠️ SKU changed for {product_name}: {old_sku}{product_sku}")
send_sku_change_notification(product_name, old_sku, product_sku, PRODUCT_URL)
last_sku_dict[product_name] = product_sku
first_run_dict[product_name] = False
# Stock check
api_stock_url = API_URL_STOCK + product_sku
logging.info(f"[{product_name}] Checking stock: {api_stock_url}")
try: try:
response = session.get(API_URL, headers=HEADERS, timeout=10) response = session.get(api_stock_url, headers=HEADERS, timeout=10)
logging.info(f"Réponse de l'API : {response.status_code}") logging.info(f"[{product_name}] Stock API response: {response.status_code}")
response.raise_for_status() response.raise_for_status()
data = response.json() stock_data = response.json()
except requests.exceptions.RequestException as e: except requests.exceptions.RequestException as e:
logging.error(f"Erreur API Stock : {e}") logging.error(f"Stock API error: {e}")
return continue
products = data.get("listMap", []) products = stock_data.get("listMap", [])
products_price = 'Prix non disponible' # Valeur par défaut products_price = "Price not available"
# Vérification de la liste des produits et récupération du prix
if isinstance(products, list) and len(products) > 0: if isinstance(products, list) and len(products) > 0:
for product in products: for p in products:
price = product.get("price", 'Prix non disponible') price = p.get("price", 'Price not available')
if price != 'Prix non disponible': if price != 'Price not available':
products_price = price # Utiliser le prix trouvé products_price = price
break # Sortir dès qu'on trouve un prix break
else: else:
logging.error("La liste des produits est vide ou mal formée.") logging.error(f"[{product_name}] Product list is empty or malformed.")
found_in_stock = set() found_in_stock = set()
# Recherche du statut et notifications selon le statut
for p in products: for p in products:
gpu_name = p.get("fe_sku", "").upper() gpu_name = p.get("fe_sku", "").upper()
is_active = p.get("is_active") == "true" is_active = p.get("is_active") == "true"
if is_active and any(target.upper() in gpu_name for target in product_upc): if is_active and any(upc.upper() in gpu_name for upc in product_upc):
found_in_stock.add(gpu_name) found_in_stock.add(gpu_name)
for gpu in product_upc: for upc in product_upc:
gpu_upper = gpu.upper() upc_upper = upc.upper()
currently_in_stock = gpu_upper in found_in_stock currently_in_stock = upc_upper in found_in_stock
previously_in_stock = global_stock_status.get(gpu_upper, False) previously_in_stock = global_stock_status_dict.get((product_name, upc_upper), False)
if currently_in_stock and not previously_in_stock: if currently_in_stock and not previously_in_stock:
product_link = PRODUCT_URL send_discord_notification(product_name, PRODUCT_URL, products_price)
send_discord_notification(gpu_upper, product_link, products_price) global_stock_status_dict[(product_name, upc_upper)] = True
global_stock_status[gpu_upper] = True logging.info(f"[{product_name}] {upc} is now in stock!")
logging.info(f"{gpu} est maintenant en stock!")
elif not currently_in_stock and previously_in_stock: elif not currently_in_stock and previously_in_stock:
product_link = PRODUCT_URL send_out_of_stock_notification(product_name, PRODUCT_URL, products_price)
send_out_of_stock_notification(gpu_upper, product_link, products_price) global_stock_status_dict[(product_name, upc_upper)] = False
global_stock_status[gpu_upper] = False logging.info(f"[{product_name}] {upc} is now out of stock.")
logging.info(f"{gpu} n'est plus en stock.") elif currently_in_stock:
logging.info(f"[{product_name}] {upc} still in stock.")
elif currently_in_stock and previously_in_stock:
logging.info(f"{gpu} est actuellement en stock.")
else: else:
logging.info(f"{gpu} est actuellement hors stock.") logging.info(f"[{product_name}] {upc} still out of stock.")
# Loop
# Boucle
if __name__ == "__main__": if __name__ == "__main__":
try:
while True: while True:
start = time.time()
check_rtx_50_founders() check_rtx_50_founders()
time.sleep(REFRESH_TIME) elapsed = time.time() - start
time.sleep(max(0, REFRESH_TIME - elapsed))
# Gracefully shut down
except KeyboardInterrupt:
logging.info("🛑 Script interrupted by user. Exiting gracefully.")
exit(0)