25 Commits
v2.5 ... 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
8 changed files with 286 additions and 261 deletions

View File

@ -1,22 +1,16 @@
# Utiliser une image de base légère de Python (ici Python 3.9)
FROM python:3.9-slim
FROM python:3.11-alpine
RUN apk add --no-cache ca-certificates
# Définir le répertoire de travail à l'intérieur du conteneur
WORKDIR /app
# Copier le script Python dans le répertoire de travail
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/
# Installer les dépendances Python à partir de 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" \
REFRESH_TIME="60"
REFRESH_TIME="30"
# Exposer un point de commande pour exécuter le script
CMD ["python", "nvidia-stock-bot.py"]

185
README.md
View File

@ -1,50 +1,49 @@
<h1 align="center"> Nvidia Stock Bot</h1>
<h1 align="center">Nvidia Stock Bot</h1>
<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">
<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>
</div>
<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">
</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)
- [Installation docker sans le dépot (rapide)](#installation-sans-le-d%C3%A9pot-avec-docker-compose)
- [Installation docker avec le dépot (développeur)](#installation-avec-le-d%C3%A9pot)
- [Installation avec Python (développeur)](#installation-avec-python)
- [Captures d'écran](#captures-d%C3%A9cran)
- [Contributeurs](#contributeurs)
- [✨ Features](#-features)
- [🐳 Docker Installation without cloning the repo (quick)](#-docker-installation-without-the-repo-quick)
- [🐙 Docker Installation with the repo (developer)](#-docker-installation-with-the-repo)
- [🐍 Python Installation (developer)](#-python-installation)
- [🖼️ Screenshots](#-screenshots)
- [🐞 Common issues](#-common-issues)
- [🧑‍💻 Contributors](#-contributors)
## Fonctionnalités
## ✨ Features
- Notification Discord `@everyone` en cas de changement du SKU (potentiel drop imminent)
- Notification Discord `@everyone` en cas de stock détecté avec modèle, prix, et lien
- Notification Discord silencieuse en cas d'absence de stock détécté
- Choix de la fréquence de la vérification
- Choix du modèle à surveiller
- Discord `@everyone` notification on SKU change (possible imminent drop)
- Discord `@everyone` notification when stock is detected, including model, price, and link
- Silent Discord notification when no stock is detected
- Selectable check frequency
- 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.
**Pré-requis**
**Requirements**
- [Docker](https://docs.docker.com/engine/install/)
**Configuration**
- Créez un dossier `nvidia-stock-bot`
- Créez le fichier `compose.yaml` dans ce dossier avec la configuration ci-dessous :
- Create a folder named `nvidia-stock-bot`
- Create a `compose.yaml` file inside that folder with the following content:
```yaml
services:
@ -53,120 +52,134 @@ services:
container_name: nvidia-stock-bot
restart: unless-stopped
environment:
- DISCORD_WEBHOOK_URL= # URL de votre webhook Discord
- PRODUCT_NAME= # Le nom exact du GPU que vous recherchez comme "RTX 5080"
- PYTHONUNBUFFERED=1 # Permet d'afficher les logs en temps réel
- PRODUCT_NAMES= # Exact GPU name (e.g. "RTX 5080, RTX 5090")
- DISCORD_ROLES= # List of Discord roles ID (e.g. "<@&12345>, <@&6789>"), in the same order than PRODUCT_NAMES values. @everyone by default.
- DISCORD_WEBHOOK_URL= # Your Discord webhook URL
- API_URL_SKU= # API listing the product
- API_URL_STOCK= # API providing stock data
- PRODUCT_URL= # GPU purchase URL
- PYTHONUNBUFFERED=1 # Enables real-time log output
command: python nvidia-stock-bot.py
```
**Variables d'environnements :**
**Environment Variables:**
| Variables | Explications | Valeurs possibles | Valeur par défaut |
|---------------------|-------------------------------------------------|--------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------|
| DISCORD_WEBHOOK_URL | URL de votre webhook Discord | Une URL | |
| REFRESH_TIME | Durée de rafraichissement du script en secondes | `60`, `30`, etc... | `30` |
| API_URL_SKU | API listant le produit | Une URL | `https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia` |
| API_URL_STOCK | API donnant le stock | Une URL | `https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus=` |
| PRODUCT_URL | URL d'achat du GPU | Une URL | `https://marketplace.nvidia.com/fr-fr/consumer/graphics-cards/?locale=fr-fr&page=1&limit=12&manufacturer=NVIDIA` |
| PRODUCT_NAME | Le nom exact du GPU que vous recherchez | `RTX 5090`, `RTX 5080` ou `RTX 5070`. | |
| TEST_MODE | Pour tester sans envoyer de notifs | `True`, `False` | `False` |
| PYTHONUNBUFFERED | #Permet d'afficher les logs en temps réel | `1`, `0` | `1` |
| 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` |
**Lancer l'image**
**Run the image**
Rendez-vous dans le dossier `nvidia-stock-bot` et lancez le conteneur :
Navigate to the `nvidia-stock-bot` folder and launch the container:
```sh
docker compose up -d
```
**Voir les logs pour vérifier le bon fonctionnement**
**Check logs to verify operation**
```sh
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)
- [Docker](https://docs.docker.com/engine/install/)
**Cloner et paramétrer**
**Clone and configure**
- Clonez le repo :
- Clone the repo:
```sh
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
docker build -t nvidia-stock-bot .
```
- Puis rendez-vous dans le dossier `nvidia-stock-bot/docker` et éditez le fichier `.env` avec :
- l'url de votre webhook discord
- les différents liens API et produits
- 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)
- Then go to `nvidia-stock-bot/docker` and edit the `.env` file with:
- Your Discord webhook URL
- The API and product URLs
- 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
docker compose up -d
```
**Voir les logs pour vérifier le bon fonctionnement**
**Check logs to verify operation**
```sh
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
- requests : `pip install requests`
- Python 3.11 or newer
- requests: `pip install requests`
**Configuration**
- Créez un environnement virtuel (exemple : `python3 -m venv nom_de_l_environnement` )
- Créez un dossier et aller dedans
- Téléchargez le script python :
- Clone the repo:
```sh
curl -o nvidia-stock-bot.py -# https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/nvidia-stock-bot.py
```
- Exportez les variables d'environnement avec votre webhook discord et le temps de rafraichissement en secondes, par exemple :
```sh
git clone https://git.djeex.fr/Djeex/nvidia-stock-bot.git
```
- 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
export DISCORD_WEBHOOK_URL="https://votre_url_discord"
export REFRESH_TIME="60"
export API_URL_SKU="https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia&gpu=RTX%205080"
export API_URL_STOCK="https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus="
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 PRODUCT_NAME="RTX 5080"
export TEST_MODE=false
```
- Lancez le script
```sh
export DISCORD_WEBHOOK_URL="https://your_discord_url"
export PRODUCT_NAMES=RTX 5080, RTX 5090
export DISCORD_ROLES=<@&12345>, <@&6789>
export REFRESH_TIME="60"
export API_URL_SKU="https://api.nvidia.partners/edge/product/search?page=1&limit=100&locale=fr-fr&Manufacturer=Nvidia&gpu=RTX%205080"
export API_URL_STOCK="https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus="
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
export PYTHONUNBUFFERED=1
```
```sh
python nvidia-stock-bot.py
```
- Run the script
## Captures d'écran
```sh
python nvidia-stock-bot.py
```
<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 - captures">
## 🖼️ Screenshots
<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>
## 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
- 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
FREQ= # frequence de rafraichissement en secondes, par défaut 30
API_URL_SKU= # API listant le produit par défaut 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 défaut https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus=
PRODUCT_URL= # URL d'achat du GPU
PRODUCT_NAME= # Le nom exact du GPU que vous recherchez comme : "RTX 5080"
DS_HOOK= # Your Discord webhook URL
FREQ= # Refresh frequency in seconds, default is 30
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 used to check stock without specifying the SKU, default is https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus=
PRODUCT_URL= # Purchase URL of the GPU
PRODUCT_NAME= # The exact name of the GPU you're looking for, e.g., "RTX 5080"

View File

@ -12,5 +12,5 @@ services:
- API_URL_STOCK=${API_URL_STOCK}
- PRODUCT_URL=${PRODUCT_URL}
- PRODUCT_NAME=${PRODUCT_NAME}
- PYTHONUNBUFFERED=1 # Permet d'afficher les logs en temps réel
command: python nvidia-stock-bot.py # Lance le script Python
- PYTHONUNBUFFERED=1 # Allow to display log in real-time
command: python nvidia-stock-bot.py # Run the script

View File

@ -5,64 +5,87 @@ import os
import re
from requests.adapters import HTTPAdapter, Retry
# Configuration du logger
# Logger configuration
logging.basicConfig(
level=logging.INFO,
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:
DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL')
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.get('API_URL_STOCK', 'https://api.store.nvidia.com/partner/v1/feinventory?locale=fr-fr&skus=')
REFRESH_TIME = int(os.environ.get('REFRESH_TIME', 30))
REFRESH_TIME = int(os.environ.get('REFRESH_TIME'))
TEST_MODE = os.environ.get('TEST_MODE', 'False').lower() == 'true'
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.get('PRODUCT_NAME')
DISCORD_ROLES = os.environ.get('DISCORD_ROLES')
PRODUCT_NAMES = os.environ.get('PRODUCT_NAMES')
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 est requis mais non défini.")
logging.error("❌ DISCORD_WEBHOOK_URL is required but not defined.")
exit(1)
if not PRODUCT_NAME:
logging.error("❌ PRODUCT_NAME est requis mais non défini.")
exit(1)
# Regex pour extraire l'ID et le token
# Regex to extract ID and token
match = re.search(r'/(\d+)/(.*)', DISCORD_WEBHOOK_URL)
if match:
webhook_id = match.group(1)
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
# Masquer derniers caractères du token
# Mask last characters of the token
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}"
# Error logging
except KeyError as e:
logging.error(f"Variable d'environnement manquante : {e}")
logging.error(f"Missing environment variable: {e}")
exit(1)
except ValueError:
logging.error("REFRESH_TIME doit être un entier valide.")
logging.error("REFRESH_TIME must be a valid integer.")
exit(1)
# Affichage des URLs et configurations
logging.info(f"GPU: {PRODUCT_NAME}")
logging.info(f"URL Webhook Discord: {wh_masked_url}")
logging.info(f"URL API SKU: {API_URL_SKU}")
logging.info(f"URL API Stock: {API_URL_STOCK}")
logging.info(f"URL produit: {PRODUCT_URL}")
logging.info(f"Temps d'actualisation: {REFRESH_TIME} secondes")
logging.info(f"Mode Test: {TEST_MODE}")
# Display URLs and configurations
logging.info(f"GPU: {PRODUCT_NAMES}")
logging.info(f"Discord Webhook URL: {wh_masked_url}")
logging.info(f"Discord Role Mention: {DISCORD_ROLES}")
logging.info(f"API URL SKU: {API_URL_SKU}")
logging.info(f"API URL Stock: {API_URL_STOCK}")
logging.info(f"Product URL: {PRODUCT_URL}")
logging.info(f"Refresh time: {REFRESH_TIME} seconds")
logging.info(f"Test Mode: {TEST_MODE}")
# Entêtes HTTP
# HTTP headers
HEADERS = {
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
"AppleWebKit/537.36 (KHTML, like Gecko) "
@ -81,30 +104,27 @@ HEADERS = {
"Sec-GPC": "1",
}
# Session avec retries
# Session with retries
session = requests.Session()
retries = Retry(total=5, backoff_factor=1, status_forcelist=[500, 502, 503, 504])
session.mount('https://', HTTPAdapter(max_retries=retries))
# Stockage de l'état des stocks
global_stock_status = {}
last_sku_dict = {}
global_stock_status_dict = {}
first_run_dict = {name: True for name in PRODUCT_NAMES}
# Stocke le dernier SKU connu
last_sku = None
first_run = True # Before calling check_rtx_50_founders
# Notifications Discord
# Discord notifications
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())
if TEST_MODE:
logging.info(f"[TEST MODE] Notification Discord: {gpu_name} disponible !")
logging.info(f"[TEST MODE] Discord Notification: {gpu_name} available!")
return
embed = {
"title": f"🚀 {PRODUCT_NAME} EN STOCK !",
"title": f"🚀 {gpu_name} IN STOCK!",
"color": 3066993,
"thumbnail": {
"url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/RTX5000.jpg"
@ -115,45 +135,52 @@ def send_discord_notification(gpu_name: str, product_link: str, products_price:
"fields": [
{
"name": "Prix",
"name": "Price",
"value": f"`{products_price}€`",
"inline": True
},
{
"name": "Heure",
"name": "Time",
"value": f"<t:{timestamp_unix}:d> <t:{timestamp_unix}:T>",
"inline": True
},
],
"description": f"**:point_right: [Acheter maintenant]({product_link})**",
"description": f"**:point_right: [Buy now]({product_link})**",
"footer": {
"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"
}
}
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:
response = requests.post(DISCORD_WEBHOOK_URL, json=payload)
if response.status_code == 204:
logging.info("✅ Notification envoyée sur Discord.")
logging.info("✅ Notification sent to Discord.")
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:
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):
# Récupérer le timestamp UNIX actuel
# Get current UNIX timestamp
timestamp_unix = int(time.time())
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
embed = {
"title": f"{PRODUCT_NAME} n'est plus en stock",
"color": 15158332, # Rouge pour hors stock
"title": f"{gpu_name} is out of stock",
"color": 15158332, # Red for out of stock
"thumbnail": {
"url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/RTX5000.jpg"
},
@ -169,7 +196,7 @@ def send_out_of_stock_notification(gpu_name: str, product_link: str, products_pr
"fields": [
{
"name": "Heure",
"name": "Time",
"value": f"<t:{timestamp_unix}:d> <t:{timestamp_unix}:T>",
"inline": True
}
@ -179,26 +206,26 @@ def send_out_of_stock_notification(gpu_name: str, product_link: str, products_pr
try:
response = requests.post(DISCORD_WEBHOOK_URL, json=payload)
if response.status_code == 204:
logging.info("Notification 'hors stock' envoyée sur Discord.")
logging.info("'Out of stock' notification sent to Discord.")
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:
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())
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
embed = {
"title": f"🔄 {PRODUCT_NAME} Changement de SKU détecté",
"title": f"🔄 {gpu_name} SKU change detected",
"url": f"{product_link}",
"description": f"**Ancien SKU** : `{old_sku}`\n**Nouveau SKU** : `{new_sku}`",
"color": 16776960, # Jaune
"description": f"**Old SKU** : `{old_sku}`\n**New SKU** : `{new_sku}`",
"color": 16776960, # Yellow
"footer": {
"text": "NviBot • JV Hardware 2.0",
@ -207,7 +234,7 @@ def send_sku_change_notification(old_sku: str, new_sku: str, product_link: str):
"fields": [
{
"name": "Heure",
"name": "Time",
"value": f"<t:{timestamp_unix}:d> <t:{timestamp_unix}:T>",
"inline": True
}
@ -215,7 +242,7 @@ def send_sku_change_notification(old_sku: str, new_sku: str, product_link: str):
}
payload = {
"content": "@everyone ⚠️ Potentiel drop imminent !",
"content": f"{DISCORD_ROLE_MAP.get(gpu_name, '@everyone')} ⚠️ Possible imminent drop!",
"username": "NviBot",
"avatar_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg",
"embeds": [embed]
@ -224,120 +251,111 @@ def send_sku_change_notification(old_sku: str, new_sku: str, product_link: str):
try:
response = requests.post(DISCORD_WEBHOOK_URL, json=payload)
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:
logging.error(f" Erreur Webhook : {response.status_code} - {response.text}")
logging.error(f"❌ Webhook error: {response.status_code} - {response.text}")
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():
global global_stock_status, last_sku, first_run
# Appel vers l'API produit pour récupérer le sku et l'upc
global last_sku_dict, global_stock_status_dict, first_run_dict
try:
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()
data = response.json()
except requests.exceptions.RequestException as e:
logging.error(f"Erreur API SKU : {e}")
logging.error(f"SKU API error: {e}")
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
for p in data['searchedProducts']['productDetails']:
gpu_name = p.get("gpu", "").strip()
# Si le GPU correspond exactement à PRODUCT_NAME
if gpu_name == PRODUCT_NAME.strip():
for p in all_products:
if p.get("gpu", "").strip() == product_name:
product_details = p
break # Sortir dès qu'on trouve le bon produit
break
if not product_details:
logging.warning(f"⚠️ Aucun produit avec le GPU '{PRODUCT_NAME}' trouvé.")
return
logging.warning(f"⚠️ No product with GPU '{product_name}' found.")
continue
# Récupérer le SKU pour le GPU trouvé
product_sku = product_details['productSKU']
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):
product_upc = [product_upc]
# Construction de l'url de l'API de stock et appel pour vérifier le statut
API_URL = API_URL_STOCK + product_sku
logging.info(f"URL de l'API de stock appelée : {API_URL}")
# Check SKU change
old_sku = last_sku_dict.get(product_name)
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:
response = session.get(API_URL, headers=HEADERS, timeout=10)
logging.info(f"Réponse de l'API : {response.status_code}")
response = session.get(api_stock_url, headers=HEADERS, timeout=10)
logging.info(f"[{product_name}] Stock API response: {response.status_code}")
response.raise_for_status()
data = response.json()
stock_data = response.json()
except requests.exceptions.RequestException as e:
logging.error(f"Erreur API Stock : {e}")
return
logging.error(f"Stock API error: {e}")
continue
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
products = stock_data.get("listMap", [])
products_price = "Price not available"
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
for p in products:
price = p.get("price", 'Price not available')
if price != 'Price not available':
products_price = price
break
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()
# 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):
if is_active and any(upc.upper() in gpu_name for upc 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)
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:
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!")
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:
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.")
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"{gpu} est actuellement hors stock.")
# Boucle
logging.info(f"[{product_name}] {upc} still out of stock.")
# Loop
if __name__ == "__main__":
try:
while True:
start = time.time()
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)