26 Commits
v2.4 ... v3.0

Author SHA1 Message Date
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 296 additions and 250 deletions

View File

@ -16,7 +16,7 @@ RUN pip install --no-cache-dir -r requirements.txt
# Définir les variables d'environnement par défaut (modifiable lors du lancement du conteneur) # 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 # Exposer un point de commande pour exécuter le script
CMD ["python", "nvidia-stock-bot.py"] CMD ["python", "nvidia-stock-bot.py"]

187
README.md
View File

@ -1,165 +1,186 @@
<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 # Permet d'afficher les logs en temps réel - PYTHONUNBUFFERED=1 # Enables real-time log output
command: python nvidia-stock-bot.py # Lance le script Python au démarrage du conteneur command: python nvidia-stock-bot.py
``` ```
**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,109 @@ 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
product_details = None all_products = data['searchedProducts']['productDetails']
for p in data['searchedProducts']['productDetails']: for product_name in PRODUCT_NAMES:
gpu_name = p.get("gpu", "").strip() product_details = None
for p in all_products:
if p.get("gpu", "").strip() == product_name:
product_details = p
break
# Si le GPU correspond exactement à PRODUCT_NAME if not product_details:
if gpu_name == PRODUCT_NAME.strip(): logging.warning(f"⚠️ No product with GPU '{product_name}' found.")
product_details = p continue
break # Sortir dès qu'on trouve le bon produit
if not product_details: product_sku = product_details['productSKU']
logging.warning(f"⚠️ Aucun produit avec le GPU '{PRODUCT_NAME}' trouvé.") product_upc = product_details.get('productUPC', "")
return if not isinstance(product_upc, list):
product_upc = [product_upc]
# Récupérer le SKU pour le GPU trouvé # Check SKU change
product_sku = product_details['productSKU'] old_sku = last_sku_dict.get(product_name)
product_upc = product_details.get('productUPC', "") 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
# Vérifier si c'est la première exécution # Stock check
if last_sku is not None and product_sku != last_sku: api_stock_url = API_URL_STOCK + product_sku
if not first_run: # Évite d'envoyer une notification au premier appel logging.info(f"[{product_name}] Checking stock: {api_stock_url}")
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é try:
last_sku = product_sku response = session.get(api_stock_url, headers=HEADERS, timeout=10)
first_run = False # Désactive la protection après la première exécution logging.info(f"[{product_name}] Stock API response: {response.status_code}")
response.raise_for_status()
if not isinstance(product_upc, list): stock_data = response.json()
product_upc = [product_upc] except requests.exceptions.RequestException as e:
logging.error(f"Stock API error: {e}")
# Construction de l'url de l'API de stock et appel pour vérifier le statut continue
API_URL = API_URL_STOCK + product_sku
logging.info(f"URL de l'API de stock appelée : {API_URL}")
try:
response = session.get(API_URL, headers=HEADERS, timeout=10)
logging.info(f"Réponse de l'API : {response.status_code}")
response.raise_for_status()
data = response.json()
except requests.exceptions.RequestException as e:
logging.error(f"Erreur API Stock : {e}")
return
products = data.get("listMap", [])
products_price = 'Prix non disponible' # Valeur par défaut
# Vérification de la liste des produits et récupération du prix
if isinstance(products, list) and len(products) > 0:
for product in products:
price = product.get("price", 'Prix non disponible')
if price != 'Prix non disponible':
products_price = price # Utiliser le prix trouvé
break # Sortir dès qu'on trouve un prix
else:
logging.error("La liste des produits est vide ou mal formée.")
found_in_stock = set()
# Recherche du statut et notifications selon le statut
for p in products:
gpu_name = p.get("fe_sku", "").upper()
is_active = p.get("is_active") == "true"
if is_active and any(target.upper() in gpu_name for target in product_upc):
found_in_stock.add(gpu_name)
for gpu in product_upc:
gpu_upper = gpu.upper()
currently_in_stock = gpu_upper in found_in_stock
previously_in_stock = global_stock_status.get(gpu_upper, False)
if currently_in_stock and not previously_in_stock:
product_link = PRODUCT_URL
send_discord_notification(gpu_upper, product_link, products_price)
global_stock_status[gpu_upper] = True
logging.info(f"{gpu} est maintenant en stock!")
elif not currently_in_stock and previously_in_stock:
product_link = PRODUCT_URL
send_out_of_stock_notification(gpu_upper, product_link, products_price)
global_stock_status[gpu_upper] = False
logging.info(f"{gpu} n'est plus en stock.")
elif currently_in_stock and previously_in_stock:
logging.info(f"{gpu} est actuellement en stock.")
products = stock_data.get("listMap", [])
products_price = "Price not available"
if isinstance(products, list) and len(products) > 0:
for p in products:
price = p.get("price", 'Price not available')
if price != 'Price not available':
products_price = price
break
else: else:
logging.info(f"{gpu} est actuellement hors stock.") logging.error(f"[{product_name}] Product list is empty or malformed.")
# Boucle found_in_stock = set()
for p in products:
gpu_name = p.get("fe_sku", "").upper()
is_active = p.get("is_active") == "true"
if is_active and any(upc.upper() in gpu_name for upc in product_upc):
found_in_stock.add(gpu_name)
for upc in product_upc:
upc_upper = upc.upper()
currently_in_stock = upc_upper in found_in_stock
previously_in_stock = global_stock_status_dict.get((product_name, upc_upper), False)
if currently_in_stock and not previously_in_stock:
send_discord_notification(product_name, PRODUCT_URL, products_price)
global_stock_status_dict[(product_name, upc_upper)] = True
logging.info(f"[{product_name}] {upc} is now in stock!")
elif not currently_in_stock and previously_in_stock:
send_out_of_stock_notification(product_name, PRODUCT_URL, products_price)
global_stock_status_dict[(product_name, upc_upper)] = False
logging.info(f"[{product_name}] {upc} is now out of stock.")
elif currently_in_stock:
logging.info(f"[{product_name}] {upc} still in stock.")
else:
logging.info(f"[{product_name}] {upc} still out of stock.")
# Loop
if __name__ == "__main__": if __name__ == "__main__":
while True: try:
check_rtx_50_founders() while True:
time.sleep(REFRESH_TIME) check_rtx_50_founders()
time.sleep(REFRESH_TIME)
# Gracefully shut down
except KeyboardInterrupt:
logging.info("🛑 Script interrupted by user. Exiting gracefully.")
exit(0)