Eldorado : Added the minimum stock threshold
It now look at all of the offers of the first page and select the one matching the criterias
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Browser\Jobs\EldoradoRobuxPriceSentry;
|
||||
|
||||
class EldoradoRobuxOffer
|
||||
{
|
||||
public int $robuxStock;
|
||||
public float $robuxPrice;
|
||||
|
||||
public int $listingIndex;
|
||||
|
||||
public function __construct(int $robuxStock, float $robuxPrice, int $listingIndex)
|
||||
{
|
||||
$this->robuxStock = $robuxStock;
|
||||
$this->robuxPrice = $robuxPrice;
|
||||
$this->listingIndex = $listingIndex;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -10,21 +10,25 @@ use App\Notification\Stringifiable\StringifiableSimpleText;
|
||||
class EldoradoRobuxPriceNotification extends Notification {
|
||||
|
||||
private float $price;
|
||||
private float $threshold;
|
||||
private float $priceThreshold;
|
||||
private string $link;
|
||||
private int $stock;
|
||||
private int|null $stockThreshold;
|
||||
|
||||
public function __construct(int $jobId, float $price, float $threshold, string $link) {
|
||||
public function __construct(int $jobId, float $price, float $priceThreshold, int $stock, int|null $stockThreshold, string $link) {
|
||||
parent::__construct($jobId);
|
||||
|
||||
$this->price = $price;
|
||||
$this->threshold = $threshold;
|
||||
$this->priceThreshold = $priceThreshold;
|
||||
$this->stock = $stock;
|
||||
$this->stockThreshold = $stockThreshold;
|
||||
$this->link = $link;
|
||||
|
||||
$this->setBody($this->generateBody());
|
||||
}
|
||||
|
||||
private function generateBody() {
|
||||
return new EldoradoRobuxPriceNotificationBody($this->price, $this->threshold, $this->link);
|
||||
return new EldoradoRobuxPriceNotificationBody($this->price, $this->priceThreshold, $this->stock, $this->stockThreshold, $this->link);
|
||||
}
|
||||
|
||||
public function getTitle(): Stringifiable {
|
||||
|
||||
@@ -7,14 +7,18 @@ use App\Notification\NotificationBody;
|
||||
class EldoradoRobuxPriceNotificationBody extends NotificationBody {
|
||||
|
||||
private float $price;
|
||||
private float $threshold;
|
||||
private float $priceThreshold;
|
||||
private int $stock;
|
||||
private int|null $stockThreshold;
|
||||
private string $link;
|
||||
|
||||
public function __construct(float $price, float $threshold, string $link) {
|
||||
public function __construct(float $price, float $priceThreshold, int $stock, int|null $stockThreshold, string $link) {
|
||||
parent::__construct();
|
||||
|
||||
$this->price = $price;
|
||||
$this->threshold = $threshold;
|
||||
$this->priceThreshold = $priceThreshold;
|
||||
$this->stock = $stock;
|
||||
$this->stockThreshold = $stockThreshold;
|
||||
$this->link = $link;
|
||||
}
|
||||
|
||||
@@ -22,13 +26,21 @@ class EldoradoRobuxPriceNotificationBody extends NotificationBody {
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toMarkdownString(): string {
|
||||
return "Le prix des Robux sur Eldorado est actuellement de **" . number_format($this->price, 5, ",", " ") . " €**/Robux, ce qui est inférieur ou égal au seuil de **" . number_format($this->threshold, 5, ",", " ") . " €**.\n\n[Voir l'offre sur Eldorado]( " . $this->link . " )";
|
||||
return "Le prix des Robux sur Eldorado est actuellement de **" . $this->floatFormat($this->price) . " €**/Robux, ce qui est inférieur ou égal au seuil de **" . $this->floatFormat($this->priceThreshold) . " €**.\nLe stock est de **" . $this->intFormat($this->stock) . "**" . ($this->stockThreshold !== null ? " (seuil : **" . $this->intFormat($this->stockThreshold) . "**)" : "") . "\n[Voir l'offre sur Eldorado]( " . $this->link . " )";
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toString(): string {
|
||||
return "Le prix des Robux sur Eldorado est actuellement de " . number_format($this->price, 5, ",", " ") . " €/Robux, ce qui est inférieur ou égal au seuil de " . number_format($this->threshold, 5, ",", " ") . " €.\n\nVoir l'offre sur Eldorado : " . $this->link;
|
||||
return "Le prix des Robux sur Eldorado est actuellement de " . $this->floatFormat($this->price) . " €/Robux, ce qui est inférieur ou égal au seuil de " . $this->floatFormat($this->priceThreshold) . " €.\nLe stock est de " . $this->intFormat($this->stock) . ($this->stockThreshold !== null ? " (seuil : " . $this->intFormat($this->stockThreshold) . ")" : "") . "\nVoir l'offre sur Eldorado : " . $this->link;
|
||||
}
|
||||
|
||||
private function floatFormat(float $number): string {
|
||||
return number_format($number, 5, ",", " ");
|
||||
}
|
||||
|
||||
private function intFormat(int $number): string {
|
||||
return number_format($number, 0, ",", " ");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user