diff --git a/Dockerfile b/Dockerfile index e572108..22ab619 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,9 +4,11 @@ RUN apk add --no-cache ca-certificates WORKDIR /app -COPY nvidia-stock-bot.py /app/ +COPY app/nvidia-stock-bot.py /app/ -COPY requirements.txt /app/ +COPY app/requirements.txt /app/ + +COPY app/localization.json /app/ RUN pip install --no-cache-dir -r requirements.txt diff --git a/README.md b/README.md index d779cad..faa6a71 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,13 @@ services: container_name: nvidia-stock-bot restart: unless-stopped environment: + # Minimal environment variables - 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 + - API_URL_SKU= # API listing the product for your country + - API_URL_STOCK= # API providing stock data for your country + - PRODUCT_URL= # GPU purchase URL for your country - PYTHONUNBUFFERED=1 # Enables real-time log output command: python nvidia-stock-bot.py ``` @@ -66,8 +67,10 @@ services: | 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_WEBHOOK_URL` | Your Discord webhook URL | `bg`, `cs`, `da`, `de`, `el`, `en`, `es`, `et`, `fi`, `fr`, `ga`, `hr`, `hu`, `it`, `lt`, `lv`, `mt`, `nl`, `pl`, `pt`, `ro`, `sk`, `sl`, `sv` | `en` | +| `DISCORD_SERVER_NAME` | The name of your server, displayed in notification's footer | A text | Shared for free | +| `DISCORD_NOTIFICATION_LANGUAGE` | Your language for notification's content | A valid URL | | | `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` | diff --git a/app/localization.json b/app/localization.json new file mode 100644 index 0000000..25a5868 --- /dev/null +++ b/app/localization.json @@ -0,0 +1,266 @@ +{ + "en": { + "in_stock_title": "🚀 {gpu_name} IN STOCK!", + "out_of_stock_title": "❌ {gpu_name} is out of stock", + "sku_change_title": "🔄 SKU change detected for {gpu_name}", + "buy_now": "**:point_right: [Buy now]({product_link})**", + "price": "Price", + "time": "Time", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Old SKU** : `{old_sku}`\n**New SKU** : `{new_sku}`", + "imminent_drop": "⚠️ Possible imminent drop!" + }, + "bg": { + "in_stock_title": "🚀 {gpu_name} ИМА НАЛИЧНОСТ!", + "out_of_stock_title": "❌ {gpu_name} е изчерпан", + "sku_change_title": "🔄 Засечена промяна на SKU за {gpu_name}", + "buy_now": "**:point_right: [Купи сега]({product_link})**", + "price": "Цена", + "time": "Час", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Стар SKU**: `{old_sku}`\n**Нов SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Възможен наближаващ спад!" + }, + "cs": { + "in_stock_title": "🚀 {gpu_name} SKLADEM!", + "out_of_stock_title": "❌ {gpu_name} je vyprodán", + "sku_change_title": "🔄 Změna SKU nalezena pro {gpu_name}", + "buy_now": "**:point_right: [Koupit nyní]({product_link})**", + "price": "Cena", + "time": "Čas", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Starý SKU**: `{old_sku}`\n**Nový SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Možný blížící se pokles!" + }, + "da": { + "in_stock_title": "🚀 {gpu_name} PÅ LAGER!", + "out_of_stock_title": "❌ {gpu_name} er udsolgt", + "sku_change_title": "🔄 SKU-ændring fundet for {gpu_name}", + "buy_now": "**:point_right: [Køb nu]({product_link})**", + "price": "Pris", + "time": "Tid", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Gammelt SKU**: `{old_sku}`\n**Nyt SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Muligt snarligt drop!" + }, + "de": { + "in_stock_title": "🚀 {gpu_name} AUF LAGER!", + "out_of_stock_title": "❌ {gpu_name} ist ausverkauft", + "sku_change_title": "🔄 SKU-Änderung erkannt für {gpu_name}", + "buy_now": "**:point_right: [Jetzt kaufen]({product_link})**", + "price": "Preis", + "time": "Zeit", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Alte SKU**: `{old_sku}`\n**Neue SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Möglicher bevorstehender Drop!" + }, + "el": { + "in_stock_title": "🚀 {gpu_name} ΔΙΑΘΕΣΙΜΟ!", + "out_of_stock_title": "❌ {gpu_name} εξαντλήθηκε", + "sku_change_title": "🔄 Ανίχνευση αλλαγής SKU για {gpu_name}", + "buy_now": "**:point_right: [Αγόρασε τώρα]({product_link})**", + "price": "Τιμή", + "time": "Ώρα", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Παλαιό SKU**: `{old_sku}`\n**Νέο SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Πιθανή επικείμενη πτώση!" + }, + "es": { + "in_stock_title": "🚀 {gpu_name} ¡EN STOCK!", + "out_of_stock_title": "❌ {gpu_name} está agotado", + "sku_change_title": "🔄 Cambio de SKU detectado para {gpu_name}", + "buy_now": "**:point_right: [Comprar ahora]({product_link})**", + "price": "Precio", + "time": "Hora", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**SKU antiguo**: `{old_sku}`\n**SKU nuevo**: `{new_sku}`", + "imminent_drop": "⚠️ ¡Posible drop inminente!" + }, + "et": { + "in_stock_title": "🚀 {gpu_name} LAOS!", + "out_of_stock_title": "❌ {gpu_name} on välja müüdud", + "sku_change_title": "🔄 Tuvastatud SKU muudatus {gpu_name}", + "buy_now": "**:point_right: [Osta kohe]({product_link})**", + "price": "Hind", + "time": "Aeg", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Vana SKU**: `{old_sku}`\n**Uus SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Võimalik varsti tulemas drop!" + }, + "fi": { + "in_stock_title": "🚀 {gpu_name} VARASTOSSA!", + "out_of_stock_title": "❌ {gpu_name} on loppunut", + "sku_change_title": "🔄 SKU-muutos havaittu {gpu_name}", + "buy_now": "**:point_right: [Osta nyt]({product_link})**", + "price": "Hinta", + "time": "Aika", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Vanha SKU**: `{old_sku}`\n**Uusi SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Mahdollinen lähestyvä drop!" + }, + "fr": { + "in_stock_title": "🚀 {gpu_name} EN STOCK !", + "out_of_stock_title": "❌ {gpu_name} est en rupture", + "sku_change_title": "🔄 Changement de SKU détecté pour {gpu_name}", + "buy_now": "**:point_right: [Acheter maintenant]({product_link})**", + "price": "Prix", + "time": "Heure", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Ancien SKU** : `{old_sku}`\n**Nouveau SKU** : `{new_sku}`", + "imminent_drop": "⚠️ Éventuel drop imminent!" + }, + "hr": { + "in_stock_title": "🚀 {gpu_name} NA ZALIHI!", + "out_of_stock_title": "❌ {gpu_name} je rasprodan", + "sku_change_title": "🔄 Promjena SKU za {gpu_name}", + "buy_now": "**:point_right: [Kupi sada]({product_link})**", + "price": "Cijena", + "time": "Vrijeme", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Stari SKU**: `{old_sku}`\n**Novi SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Mogući nadolazeći drop!" + }, + "hu": { + "in_stock_title": "🚀 {gpu_name} RAKTÁRON!", + "out_of_stock_title": "❌ {gpu_name} elfogyott", + "sku_change_title": "🔄 SKU változás észlelve: {gpu_name}", + "buy_now": "**:point_right: [Vásárlás most]({product_link})**", + "price": "Ár", + "time": "Idő", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Régi SKU**: `{old_sku}`\n**Új SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Lehetséges közelgő drop!" + }, + "ga": { + "in_stock_title": "🚀 {gpu_name} I STOC!", + "out_of_stock_title": "❌ níl {gpu_name} ar fáil", + "sku_change_title": "🔄 SKU athraithe aimsithe do {gpu_name}", + "buy_now": "**:point_right: [Ceannaigh anois]({product_link})**", + "price": "Praghas", + "time": "Am", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Sean SKU**: `{old_sku}`\n**SKU Nua**: `{new_sku}`", + "imminent_drop": "⚠️ I bhfad is gaire drop féideartha!" + }, + "it": { + "in_stock_title": "🚀 {gpu_name} DISPONIBILE!", + "out_of_stock_title": "❌ {gpu_name} esaurito", + "sku_change_title": "🔄 Modifica SKU rilevata per {gpu_name}", + "buy_now": "**:point_right: [Acquista ora]({product_link})**", + "price": "Prezzo", + "time": "Ora", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Vecchio SKU**: `{old_sku}`\n**Nuovo SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Possibile drop imminente!" + }, + "lv": { + "in_stock_title": "🚀 {gpu_name} KRĀJUMĀ!", + "out_of_stock_title": "❌ {gpu_name} nav pieejams", + "sku_change_title": "🔄 SKU izmaiņas: {gpu_name}", + "buy_now": "**:point_right: [Pirkt tagad]({product_link})**", + "price": "Cena", + "time": "Laiks", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Vecais SKU**: `{old_sku}`\n**Jaunais SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Iespējams gaidāms drop!" + }, + "lt": { + "in_stock_title": "🚀 {gpu_name} SANDĖLYJE!", + "out_of_stock_title": "❌ {gpu_name} išparduotas", + "sku_change_title": "🔄 SKU pakeitimas aptiktas: {gpu_name}", + "buy_now": "**:point_right: [Pirkti dabar]({product_link})**", + "price": "Kaina", + "time": "Laikas", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Senas SKU**: `{old_sku}`\n**Naujas SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Rugsėjo drop'as gali artėti!" + }, + "mt": { + "in_stock_title": "🚀 {gpu_name} F'ĠESTA!", + "out_of_stock_title": "❌ {gpu_name} mhux disponibbli", + "sku_change_title": "🔄 Bidla SKU skoperta għal {gpu_name}", + "buy_now": "**:point_right: [Ixtri issa]({product_link})**", + "price": "Prezz", + "time": "Ħin", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**SKU Antik**: `{old_sku}`\n**SKU Ġdid**: `{new_sku}`", + "imminent_drop": "⚠️ Possibbli drop imminenti!" + }, + "nl": { + "in_stock_title": "🚀 {gpu_name} OP VOORRAAD!", + "out_of_stock_title": "❌ {gpu_name} is uitverkocht", + "sku_change_title": "🔄 SKU-wijziging gedetecteerd voor {gpu_name}", + "buy_now": "**:point_right: [Nu kopen]({product_link})**", + "price": "Prijs", + "time": "Tijd", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Oude SKU**: `{old_sku}`\n**Nieuwe SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Mogelijke aanstaande drop!" + }, + "pl": { + "in_stock_title": "🚀 {gpu_name} DOSTĘPNE!", + "out_of_stock_title": "❌ {gpu_name} jest niedostępny", + "sku_change_title": "🔄 Wykryto zmianę SKU dla {gpu_name}", + "buy_now": "**:point_right: [Kup teraz]({product_link})**", + "price": "Cena", + "time": "Czas", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Stare SKU**: `{old_sku}`\n**Nowe SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Możliwy nadchodzący drop!" + }, + "pt": { + "in_stock_title": "🚀 {gpu_name} EM STOCK!", + "out_of_stock_title": "❌ {gpu_name} esgotado", + "sku_change_title": "🔄 Alteração de SKU detectada para {gpu_name}", + "buy_now": "**:point_right: [Comprar agora]({product_link})**", + "price": "Preço", + "time": "Hora", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**SKU antigo**: `{old_sku}`\n**SKU novo**: `{new_sku}`", + "imminent_drop": "⚠️ Possível drop iminente!" + }, + "ro": { + "in_stock_title": "🚀 {gpu_name} ÎN STOC!", + "out_of_stock_title": "❌ {gpu_name} este epuizat", + "sku_change_title": "🔄 Schimbare SKU detectată pentru {gpu_name}", + "buy_now": "**:point_right: [Cumpără acum]({product_link})**", + "price": "Preț", + "time": "Timp", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**SKU vechi**: `{old_sku}`\n**SKU nou**: `{new_sku}`", + "imminent_drop": "⚠️ Posibil drop iminent!" + }, + "sk": { + "in_stock_title": "🚀 {gpu_name} NA SKLADE!", + "out_of_stock_title": "❌ {gpu_name} je vypredaný", + "sku_change_title": "🔄 Zmena SKU zistená pre {gpu_name}", + "buy_now": "**:point_right: [Kúp teraz]({product_link})**", + "price": "Cena", + "time": "Čas", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Starý SKU**: `{old_sku}`\n**Nový SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Možný nadchádzajúci drop!" + }, + "sl": { + "in_stock_title": "🚀 {gpu_name} NA ZALOGI!", + "out_of_stock_title": "❌ {gpu_name} je razprodan", + "sku_change_title": "🔄 Sprememba SKU zaznana za {gpu_name}", + "buy_now": "**:point_right: [Kupi zdaj]({product_link})**", + "price": "Cena", + "time": "Čas", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Stari SKU**: `{old_sku}`\n**Novi SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Možen prihajajoči drop!" + }, + "sv": { + "in_stock_title": "🚀 {gpu_name} I LAGER!", + "out_of_stock_title": "❌ {gpu_name} är slut", + "sku_change_title": "🔄 SKU-ändring upptäckt för {gpu_name}", + "buy_now": "**:point_right: [Köp nu]({product_link})**", + "price": "Pris", + "time": "Tid", + "footer": "NviBot • {DISCORD_SERVER_NAME}", + "sku_description": "**Gammalt SKU**: `{old_sku}`\n**Nytt SKU**: `{new_sku}`", + "imminent_drop": "⚠️ Möjlig förestående drop!" + } +} diff --git a/nvidia-stock-bot.py b/app/nvidia-stock-bot.py similarity index 87% rename from nvidia-stock-bot.py rename to app/nvidia-stock-bot.py index f41f719..301986b 100644 --- a/nvidia-stock-bot.py +++ b/app/nvidia-stock-bot.py @@ -3,6 +3,7 @@ import logging import time import os import re +import json from requests.adapters import HTTPAdapter, Retry # Logger configuration @@ -15,6 +16,7 @@ logging.info("Script started") # Retrieve environment variables try: DISCORD_WEBHOOK_URL = os.environ.get('DISCORD_WEBHOOK_URL') + DISCORD_SERVER_NAME = os.environ.get('DISCORD_SERVER_NAME', 'Shared for free') 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')) @@ -75,6 +77,36 @@ except ValueError: logging.error("REFRESH_TIME must be a valid integer.") exit(1) +# Localization +try: + with open("localization.json", "r", encoding="utf-8") as f: + localization = json.load(f) +except FileNotFoundError: + logging.error("❌ localization.json file not found.") + exit(1) + +language = os.environ.get("DISCORD_NOTIFICATION_LANGUAGE", "en").lower() +loc = localization.get(language, localization["en"]) + +if language not in localization: + logging.warning(f"⚠️ Language '{language}' not found. Falling back to English.") + +in_stock_title = loc["in_stock_title"] +out_of_stock_title = loc["out_of_stock_title"] +sku_change_title = loc["sku_change_title"] +buy_now = loc["buy_now"] +price_label = loc["price"] +time_label = loc["time"] +footer = loc["footer"] +sku_description = loc["sku_description"] +imminent_drop = loc["imminent_drop"] + +required_keys = ["in_stock_title", "out_of_stock_title", "sku_change_title", "buy_now", "price", "time", "footer", "sku_description", "imminent_drop"] +for key in required_keys: + if key not in loc: + logging.error(f"❌ Missing localization key: '{key}' in language '{language}'") + exit(1) + # Display URLs and configurations logging.info(f"GPU: {PRODUCT_NAMES}") logging.info(f"Discord Webhook URL: {wh_masked_url}") @@ -124,7 +156,7 @@ def send_discord_notification(gpu_name: str, product_link: str, products_price: return embed = { - "title": f"🚀 {gpu_name} IN STOCK!", + "title": in_stock_title.format(gpu_name=gpu_name), "color": 3066993, "thumbnail": { "url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/RTX5000.jpg" @@ -135,20 +167,20 @@ def send_discord_notification(gpu_name: str, product_link: str, products_price: "fields": [ { - "name": "Price", + "name": price_label, "value": f"`{products_price}€`", "inline": True }, { - "name": "Time", + "name": time_label, "value": f" ", "inline": True }, ], - "description": f"**:point_right: [Buy now]({product_link})**", + "description": buy_now.format(product_link=product_link), "footer": { - "text": "NviBot • JV Hardware 2.0", + "text": footer.format(DISCORD_SERVER_NAME=DISCORD_SERVER_NAME), "icon_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg" } } @@ -179,7 +211,7 @@ def send_out_of_stock_notification(gpu_name: str, product_link: str, products_pr return embed = { - "title": f"❌ {gpu_name} is out of stock", + "title": out_of_stock_title.format(gpu_name=gpu_name), "color": 15158332, # Red for out of stock "thumbnail": { "url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/RTX5000.jpg" @@ -190,13 +222,13 @@ def send_out_of_stock_notification(gpu_name: str, product_link: str, products_pr }, "footer": { - "text": "NviBot • JV Hardware 2.0", + "text": footer.format(DISCORD_SERVER_NAME=DISCORD_SERVER_NAME), "icon_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg" }, "fields": [ { - "name": "Time", + "name": time_label, "value": f" ", "inline": True } @@ -222,19 +254,19 @@ def send_sku_change_notification(gpu_name: str, old_sku: str, new_sku: str, prod return embed = { - "title": f"🔄 {gpu_name} SKU change detected", + "title": sku_change_title.format(gpu_name=gpu_name), "url": f"{product_link}", - "description": f"**Old SKU** : `{old_sku}`\n**New SKU** : `{new_sku}`", + "description": sku_description.format(old_sku=old_sku, new_sku=new_sku), "color": 16776960, # Yellow "footer": { - "text": "NviBot • JV Hardware 2.0", + "text": footer.format(DISCORD_SERVER_NAME=DISCORD_SERVER_NAME), "icon_url": "https://git.djeex.fr/Djeex/nvidia-stock-bot/raw/branch/main/assets/img/ds_wh_pp.jpg" }, "fields": [ { - "name": "Time", + "name": time_label, "value": f" ", "inline": True } @@ -242,7 +274,7 @@ def send_sku_change_notification(gpu_name: str, old_sku: str, new_sku: str, prod } payload = { - "content": f"{DISCORD_ROLE_MAP.get(gpu_name, '@everyone')} ⚠️ Possible imminent drop!", + "content": 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] diff --git a/requirements.txt b/app/requirements.txt similarity index 100% rename from requirements.txt rename to app/requirements.txt