Eldorado : Added the minimum stock threshold
All checks were successful
Test, build and push image to registry / phpunit-tests (push) Successful in 3m25s
Test, build and push image to registry / build-image (push) Successful in 2m59s

It now look at all of the offers of the first page and select the one matching the criterias
This commit is contained in:
2025-12-20 16:20:07 +01:00
parent 48fdc2f6f0
commit 5e6164016c
5 changed files with 223 additions and 48 deletions

View File

@@ -3,23 +3,16 @@
namespace App\Browser\Jobs\EldoradoRobuxPriceSentry;
use App\Browser\BrowserJob;
use App\Browser\JobDebugScreenshot;
use App\Browser\Jobs\InstagramRepost\DescriptionPipeline\InstagramDescriptionPipeline;
use App\Models\InstagramNotification;
use App\Models\InstagramRepost;
use App\Models\Job;
use App\Models\JobArtifact;
use App\Models\JobRun;
use App\Notification\Notifications\JobDebugNotification;
use App\Notification\Notifications\SimpleNotification;
use App\Notification\Providers\AllNotification;
use App\Services\Instagram\NotificationTypeDetector;
use App\Browser\Jobs\EldoradoRobuxPriceSentry\EldoradoRobuxOffer;
use Facebook\WebDriver\WebDriverBy;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Laravel\Dusk\Browser;
use App\Services\AIPrompt\OpenAPIPrompt;
class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUntilProcessing
{
@@ -28,8 +21,10 @@ class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUn
public $timeout = 600; // 10 minutes
private const APPROXIMATIVE_RUNNING_MINUTES = 20;
private const LINK = "https://www.eldorado.gg/fr/buy-robux/g/70-0-0";
protected const int LOWEST_PRICE_INDEX = -1;
protected string $downloadFolder = "app/Browser/downloads/EldoradoRobuxPriceSentry/";
protected Collection $jobInfos;
protected JobRun $jobRun;
@@ -90,6 +85,128 @@ class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUn
}
private function sendPrices(Browser $browser): void
{
$offers = $this->getOffers($browser);
$priceThreshold = $this->textToFloat($this->jobInfos->get("eldorado_robux_price_threshold"));
$stockThreshold = $this->jobInfos->get("eldorado_stock_quantity_threshold") == null ?
null :
$this->getIntFromText($this->jobInfos->get("eldorado_stock_quantity_threshold"));
foreach ($offers as $offer) {
Log::info("EldoradoRobuxPriceSentryJob: found offer - Price: " . $offer->robuxPrice . " €, Stock: " . $offer->robuxStock);
// Checks
if ($offer->robuxPrice > $priceThreshold || ($stockThreshold !== null && $offer->robuxStock < $stockThreshold)) {
continue; // Not matching the criteria
}
// Logging and artifact
Log::info("EldoradoRobuxPriceSentryJob: lowest price = $offer->robuxPrice €, threshold = $priceThreshold € - stock = $offer->robuxStock " . ($stockThreshold !== null ? "- stock threshold = $stockThreshold" : ""));
$this->jobRun->addArtifact(new JobArtifact([
"name" => "Trouvé le prix le plus bas",
"content" => "Prix le plus bas : $offer->robuxPrice €/Robux - Seuil défini : $priceThreshold €/Robux
Stock disponible : $offer->robuxStock " . ($stockThreshold !== null ? "- Seuil de stock défini : $stockThreshold" : ""),
]));
// get the link
$offerLink = $this->getLinkFromOfferIndex($browser, $offer->listingIndex);
// Send alert
$this->sendAlertNotification(
$offer->robuxPrice,
$priceThreshold,
$offer->robuxStock,
$stockThreshold,
$offerLink
);
Log::info("EldoradoRobuxPriceSentryJob: alert sent");
return; // Stops at first matching offer
}
Log::info("EldoradoRobuxPriceSentryJob: no alert sent");
}
private function sendAlertNotification(float $lowestPrice, float $priceThreshold, int $stock, int|null $stockThreshold, string $link): void
{
$options = [];
if ($this->jobInfos->get("eldorado_robux_price_discord_webhook") !== null) { // Custom discord webhook
$options["discord_webhook_url"] = $this->jobInfos->get("eldorado_robux_price_discord_webhook");
}
AllNotification::send(
new EldoradoRobuxPriceNotification(
$this->jobId,
$lowestPrice,
$priceThreshold,
$stock,
$stockThreshold,
$link
),
$options
);
$this->jobRun->addArtifact(new JobArtifact([
"name" => "Envoyé une alerte",
"content" => ""
]));
}
/**
* Retrieve the url for the given listing index.
* WILL NOT REDIRECT BACK TO THE OFFERS PAGE.
* @param Browser $browser
* @param int $listingIndex
* @return string
*/
private function getLinkFromOfferIndex(Browser $browser, int $listingIndex): string
{
if ($listingIndex == self::LOWEST_PRICE_INDEX) {
return self::LINK;
}
$offerCardElements = $this->getOffersCardElements($browser);
$offerCardElement = $offerCardElements[$listingIndex];
// Click the card element
$offerCardElement->click();
sleep(2);
// return the current url
return $browser->driver->getCurrentURL();
}
/**
* Get the list of offers on the page
* @return EldoradoRobuxOffer[]
*/
private function getOffers(Browser $browser): array
{
$offers = [];
// First offer (lowest price) (special case)
$firstOffer = $this->getFirstOffer($browser);
$offers[] = $firstOffer;
// Other offers
$offerCardElements = $this->getOffersCardElements($browser);
foreach ($offerCardElements as $index => $offerCardElement) {
$priceElement = $offerCardElement->findElement(WebDriverBy::xpath('.//eld-offer-price/strong'));
$priceText = $priceElement->getText(); // Ex: " 0,00520 € "
$price = $this->textToFloat($priceText);
// span.value next to a span.label with the text containing "stock"
$stockElement = $offerCardElement->findElement(WebDriverBy::xpath('.//span[@class="label" and contains(text(), "stock")]/following-sibling::*[@class="value"]'));
$stockText = $stockElement->getText(); // Ex: "En stock : 5000 unités"
$stock = $this->getIntFromText($stockText);
$offers[] = new EldoradoRobuxOffer(
robuxStock: $stock,
robuxPrice: $price,
listingIndex: $index
);
}
return $offers;
}
private function getFirstOffer(Browser $browser): EldoradoRobuxOffer
{
$lowestPriceElement = $browser->driver->findElement(WebDriverBy::xpath('(//eld-offer-price)[2]/strong'));
$lowestPriceText = $lowestPriceElement->getText(); // Ex: " 0,00478 € "
@@ -97,43 +214,27 @@ class EldoradoRobuxPriceSentryJob extends BrowserJob implements ShouldBeUniqueUn
//$lowestPrice = $lowestPrice / 1000; // Price per Robux
// TODO : Look at the entire text to try to understand if it is per 1k or per single Robux
$threshold = $this->textToFloat($this->jobInfos->get("eldorado_robux_price_threshold"));
$lowestStockElement = $browser->driver->findElement(WebDriverBy::xpath('(//eld-quantity-details-wrapper)[1]//div[@class="quantity"]'));
$lowestStockText = $lowestStockElement->getText(); // Ex: "En stock : 9942199 unité"
$lowestStock = $this->getIntFromText($lowestStockText);
Log::info("EldoradoRobuxPriceSentryJob: lowest price = $lowestPrice €, threshold = $threshold");
$this->jobRun->addArtifact(new JobArtifact([
"name" => "Trouvé le prix le plus bas",
"content" => "Prix le plus bas : $lowestPrice €/Robux - Seuil défini : $threshold €/Robux"
]));
return new EldoradoRobuxOffer($lowestStock, $lowestPrice, self::LOWEST_PRICE_INDEX);
}
if ($lowestPrice <= $threshold) {
$options = [];
if ($this->jobInfos->get("eldorado_robux_price_discord_webhook") !== null) { // Custom discord webhook
$options["discord_webhook_url"] = $this->jobInfos->get("eldorado_robux_price_discord_webhook");
}
AllNotification::send(
new EldoradoRobuxPriceNotification(
$this->jobId,
$lowestPrice,
$threshold,
self::LINK
),
$options
);
$this->jobRun->addArtifact(new JobArtifact([
"name" => "Envoyé une alerte",
"content" => ""
]));
Log::info("EldoradoRobuxPriceSentryJob: alert sent");
} else {
Log::info("EldoradoRobuxPriceSentryJob: no alert sent");
}
private function getOffersCardElements(Browser $browser): array
{
return $browser->driver->findElements(WebDriverBy::xpath('//div[contains(@class, "offer-seller-card")]'));
}
private function textToFloat(string $text): float
{
return floatval(str_replace(["", ","], ["", "."], trim($text)));
}
private function getIntFromText(string $text): int
{
$text = str_replace(["", " "], "", $text); // Remove spaces
preg_match('/\d+/', $text, $matches);
return intval($matches[0]);
}
}