Compare commits
6 Commits
feature/ha
...
40095ca7d6
| Author | SHA1 | Date | |
|---|---|---|---|
| 40095ca7d6 | |||
| 22e4034ae3 | |||
| 03b74aa33d | |||
| e1340d45ed | |||
| 4983f868df | |||
| f2a9bdf993 |
@@ -11,6 +11,7 @@ use Symfony\Component\Console\Command\Command;
|
|||||||
use Symfony\Component\Console\Input\InputInterface;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Input\InputOption;
|
use Symfony\Component\Console\Input\InputOption;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use function Symfony\Component\Clock\now;
|
||||||
|
|
||||||
#[AsCommand(name: 'app:migrate', description: 'Create database and rum migrations')]
|
#[AsCommand(name: 'app:migrate', description: 'Create database and rum migrations')]
|
||||||
class Migrate extends Command
|
class Migrate extends Command
|
||||||
@@ -39,6 +40,8 @@ class Migrate extends Command
|
|||||||
$this->createPricesTable();
|
$this->createPricesTable();
|
||||||
$this->createStocksTable();
|
$this->createStocksTable();
|
||||||
$this->addColumns();
|
$this->addColumns();
|
||||||
|
$this->createCountriesTable();
|
||||||
|
$this->index();
|
||||||
|
|
||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
@@ -77,6 +80,47 @@ class Migrate extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function createCountriesTable(): void
|
||||||
|
{
|
||||||
|
if (!Capsule::schema()->hasTable('countries')) {
|
||||||
|
Capsule::schema()->create('countries', function (Blueprint $table) {
|
||||||
|
$table->increments('id');
|
||||||
|
$table->text('countryName');
|
||||||
|
$table->text('productsUrl');
|
||||||
|
$table->text('cultureCode');
|
||||||
|
$table->text('currency');
|
||||||
|
$table->text('locale');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$id = Capsule::table('countries')->insertGetId(
|
||||||
|
[
|
||||||
|
'countryName' => 'Poland',
|
||||||
|
'productsUrl' => 'https://pl.ryobitools.eu/api/product-listing/get-products',
|
||||||
|
'cultureCode' => 'pl-PL',
|
||||||
|
'currency' => 'PLN',
|
||||||
|
'locale' => 'pl',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
Capsule::table('countries')->insert([
|
||||||
|
'countryName' => 'UK',
|
||||||
|
'productsUrl' => 'https://uk.ryobitools.eu/api/product-listing/get-products',
|
||||||
|
'cultureCode' => 'en-GB',
|
||||||
|
'currency' => 'GBP',
|
||||||
|
'locale' => 'en',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!Capsule::schema()->hasColumn('products', 'country_id')) {
|
||||||
|
Capsule::schema()->table('products', function (Blueprint $table) use ($id) {
|
||||||
|
$table->foreignId('country_id')->default($id)->references('id')->on('countries');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function createStocksTable(): void
|
public function createStocksTable(): void
|
||||||
{
|
{
|
||||||
if (!Capsule::schema()->hasTable('stocks')) {
|
if (!Capsule::schema()->hasTable('stocks')) {
|
||||||
@@ -127,4 +171,12 @@ class Migrate extends Command
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function index(): void
|
||||||
|
{
|
||||||
|
Capsule::schema()->table('products', function (Blueprint $table) {
|
||||||
|
$table->integer('skuID')->unique(false)->change();
|
||||||
|
$table->unique(['skuID', 'country_id']);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Krzysiej\RyobiCrawler\Command;
|
|||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use Illuminate\Database\Capsule\Manager as Capsule;
|
use Illuminate\Database\Capsule\Manager as Capsule;
|
||||||
use Illuminate\Support\Facades\Date;
|
use Krzysiej\RyobiCrawler\Models\Country;
|
||||||
use Krzysiej\RyobiCrawler\Models\Price;
|
use Krzysiej\RyobiCrawler\Models\Price;
|
||||||
use Krzysiej\RyobiCrawler\Models\Product;
|
use Krzysiej\RyobiCrawler\Models\Product;
|
||||||
use Krzysiej\RyobiCrawler\Models\Stock;
|
use Krzysiej\RyobiCrawler\Models\Stock;
|
||||||
@@ -36,16 +36,21 @@ class ScrapeWebsite extends Command
|
|||||||
{
|
{
|
||||||
$output->writeln('Scrape products');
|
$output->writeln('Scrape products');
|
||||||
$progress = new ProgressBar($output);
|
$progress = new ProgressBar($output);
|
||||||
$progress->start();
|
$countries = Country::all();
|
||||||
$products = $this->getProducts();
|
foreach($countries as $country) {
|
||||||
$progress->setMaxSteps(count($products));
|
$output->writeln('Country name: ' . $country->countryName."\n");
|
||||||
foreach ($products as $product) {
|
$progress->start();
|
||||||
$this->saveProduct($product);
|
$products = $this->getProducts($country);
|
||||||
$progress->advance();
|
$progress->setMaxSteps(count($products));
|
||||||
|
foreach ($products as $product) {
|
||||||
|
$this->saveProduct($product, $country);
|
||||||
|
$progress->advance();
|
||||||
|
}
|
||||||
|
$progress->finish();
|
||||||
|
$output->writeln('');
|
||||||
|
$output->writeln('Scrape products - DONE');
|
||||||
|
$output->writeln('');
|
||||||
}
|
}
|
||||||
$progress->finish();
|
|
||||||
$output->writeln('Scrape products - DONE');
|
|
||||||
$output->writeln('');
|
|
||||||
|
|
||||||
$output->writeln('Update prices');
|
$output->writeln('Update prices');
|
||||||
$products = Product::all();
|
$products = Product::all();
|
||||||
@@ -69,18 +74,19 @@ class ScrapeWebsite extends Command
|
|||||||
return Command::SUCCESS;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getProducts(): array
|
private function getProducts(Country $country): array
|
||||||
{
|
{
|
||||||
$products = [];
|
$products = [];
|
||||||
$page = 0;
|
$page = 0;
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try {
|
try {
|
||||||
$res = $this->client->request('POST', 'https://pl.ryobitools.eu/api/product-listing/get-products', [
|
$res = $this->client->request('POST', $country->productsUrl, [
|
||||||
'form_params' => [
|
'form_params' => [
|
||||||
"includePreviousPages" => false,
|
"includePreviousPages" => false,
|
||||||
"pageIndex" => $page,
|
"pageIndex" => $page,
|
||||||
"pageSize" => 100,
|
"pageSize" => 100,
|
||||||
"cultureCode" => "pl-PL",
|
"cultureCode" => $country->cultureCode,
|
||||||
]
|
]
|
||||||
]);
|
]);
|
||||||
$responseObject = json_decode($res->getBody()->getContents());
|
$responseObject = json_decode($res->getBody()->getContents());
|
||||||
@@ -95,10 +101,14 @@ class ScrapeWebsite extends Command
|
|||||||
return $products;
|
return $products;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function saveProduct(\stdClass $product): void
|
private function saveProduct(\stdClass $product, Country $country): void
|
||||||
{
|
{
|
||||||
|
// if ($product->skuID == 0) {
|
||||||
|
// dump([$product->skuID, $product->name]);
|
||||||
|
// }
|
||||||
|
// return;
|
||||||
/** @var Product $productModel */
|
/** @var Product $productModel */
|
||||||
$productModel = Product::firstOrNew(['skuID' => $product->skuID]);
|
$productModel = Product::firstOrNew(['skuID' => $product->skuID, 'country_id' => $country->id]);
|
||||||
|
|
||||||
$productModel->skuID = $product->skuID;
|
$productModel->skuID = $product->skuID;
|
||||||
$productModel->name = $product->name;
|
$productModel->name = $product->name;
|
||||||
@@ -111,6 +121,7 @@ class ScrapeWebsite extends Command
|
|||||||
$productModel->url = $product->url;
|
$productModel->url = $product->url;
|
||||||
$productModel->lastSeen = date("Y-m-d");
|
$productModel->lastSeen = date("Y-m-d");
|
||||||
$productModel->touch('updated_at');
|
$productModel->touch('updated_at');
|
||||||
|
$productModel->country()->associate($country);
|
||||||
$productModel->save();
|
$productModel->save();
|
||||||
$priceExists = $productModel->price()->whereRaw("strftime('%Y-%m-%d', created_at) = ?", [date('Y-m-d')])->exists();
|
$priceExists = $productModel->price()->whereRaw("strftime('%Y-%m-%d', created_at) = ?", [date('Y-m-d')])->exists();
|
||||||
|
|
||||||
|
|||||||
24
src/Models/Country.php
Normal file
24
src/Models/Country.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Krzysiej\RyobiCrawler\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property string $countryName
|
||||||
|
* @property string $productsUrl
|
||||||
|
* @property string $cultureCode
|
||||||
|
* @property string $currency
|
||||||
|
* @property string $locale
|
||||||
|
*/
|
||||||
|
class Country extends Model
|
||||||
|
{
|
||||||
|
public $timestamps = true;
|
||||||
|
|
||||||
|
public function products(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Product::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace Krzysiej\RyobiCrawler\Models;
|
|||||||
use Carbon\Traits\Date;
|
use Carbon\Traits\Date;
|
||||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
|
||||||
@@ -32,6 +33,11 @@ class Product extends Model
|
|||||||
public $timestamps = true;
|
public $timestamps = true;
|
||||||
public $fillable = ['skuID'];
|
public $fillable = ['skuID'];
|
||||||
|
|
||||||
|
public function country(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Country::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function price(): HasMany
|
public function price(): HasMany
|
||||||
{
|
{
|
||||||
return $this->hasMany(Price::class);
|
return $this->hasMany(Price::class);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
<td><img src='{{ product.image }}&width=150' class='border rounded p-1' alt='{{ product.name }}'/></td>
|
<td><img src='{{ product.image }}&width=150' class='border rounded p-1' alt='{{ product.name }}'/></td>
|
||||||
<td>
|
<td>
|
||||||
<a href='{{ path('app_product', {'productId': product.id}) }}' class="text-decoration-none">{{ product.name }}</a>
|
<a href='{{ path('app_product', {'productId': product.id}) }}' class="text-decoration-none">{{ product.name }}</a>
|
||||||
<span class="badge text-bg-light">{{ product.subTitle }}</span>
|
<span class="badge text-bg-light"><a href="{{ path('app_search', {'search': product.subTitle}) }}" class="link-underline link-underline-opacity-0 link-dark">{{ product.subTitle }}</a></span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';">
|
<nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';">
|
||||||
@@ -44,9 +44,9 @@
|
|||||||
</thead>
|
</thead>
|
||||||
{% for price in product.price %}
|
{% for price in product.price %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ price.price | format_currency('PLN', {}, 'pl') }}</td>
|
<td>{{ price.price | format_currency(product.country.currency, {}, product.country.locale) }}</td>
|
||||||
<td>{{ price.lowestProductPrice30Days | format_currency('PLN', {}, 'pl') }}</td>
|
<td>{{ price.lowestProductPrice30Days | format_currency(product.country.currency, {}, product.country.locale) }}</td>
|
||||||
<td>{{ price.productStandardPrice | format_currency('PLN', {}, 'pl') }}</td>
|
<td>{{ price.productStandardPrice | format_currency(product.country.currency, {}, product.country.locale) }}</td>
|
||||||
<td>{{ price.created_at }}</td>
|
<td>{{ price.created_at }}</td>
|
||||||
<td>{{ (product.stock | findByCreatedAtDate(price.created_at | slice(0,10))).stock ?? '' }}</td>
|
<td>{{ (product.stock | findByCreatedAtDate(price.created_at | slice(0,10))).stock ?? '' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
labels: ['{{ price_dates|raw }}'],
|
labels: ['{{ price_dates|raw }}'],
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Price (PLN)',
|
label: 'Price ({{ product.country.currency }})',
|
||||||
data: {{ price_list|raw }},
|
data: {{ price_list|raw }},
|
||||||
yAxisID: 'yPrice',
|
yAxisID: 'yPrice',
|
||||||
tension: 0.1,
|
tension: 0.1,
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
beginAtZero: true,
|
beginAtZero: true,
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: 'Price (PLN)'
|
text: 'Price ({{ product.country.currency }})'
|
||||||
},
|
},
|
||||||
grid: {
|
grid: {
|
||||||
drawOnChartArea: false
|
drawOnChartArea: false
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
{% if product.isNew() %}
|
{% if product.isNew() %}
|
||||||
<span class="badge text-bg-success">is new</span>
|
<span class="badge text-bg-success">is new</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="badge text-bg-light">{{ product.subTitle }}</span>
|
<span class="badge text-bg-light"><a href="{{ path('app_search', {'search': product.subTitle}) }}" class="link-underline link-underline-opacity-0 link-dark">{{ product.subTitle }}</a></span>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';" >
|
<nav aria-label="breadcrumb" style="--bs-breadcrumb-divider: '>';" >
|
||||||
@@ -48,16 +48,16 @@
|
|||||||
<td class="align-middle"><a href='https://pl.ryobitools.eu/{{ product.url }}'>link</a></td>
|
<td class="align-middle"><a href='https://pl.ryobitools.eu/{{ product.url }}'>link</a></td>
|
||||||
<td class="align-middle text-end">
|
<td class="align-middle text-end">
|
||||||
{% if product.isDiscontinued() or product.priceCurrent == product.productStandardPrice %}
|
{% if product.isDiscontinued() or product.priceCurrent == product.productStandardPrice %}
|
||||||
{{ product.priceLowest | format_currency('PLN', {}, 'pl') }}
|
{{ product.priceLowest | format_currency(product.country.currency, {}, product.country.locale) }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if product.priceLowest != product.priceCurrent %}{{ product.priceLowest | format_currency('PLN', {}, 'pl') }}{%else%}<span class="badge text-bg-info">now lowest</span>{% endif %}</td>
|
{% if product.priceLowest != product.priceCurrent %}{{ product.priceLowest | format_currency(product.country.currency, {}, product.country.locale) }}{%else%}<span class="badge text-bg-info">now lowest</span>{% endif %}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<td class="align-middle text-end">{{ product.priceCurrent | format_currency('PLN', {}, 'pl') }}</td>
|
<td class="align-middle text-end">{{ product.priceCurrent | format_currency(product.country.currency, {}, product.country.locale) }}</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="d-flex flex-row">
|
<div class="d-flex flex-row">
|
||||||
{% if product.priceCurrent != product.productStandardPrice %}<span
|
{% if product.priceCurrent != product.productStandardPrice %}<span
|
||||||
class="badge text-bg-warning text-decoration-line-through flex-fill">{{ product.productStandardPrice | format_currency('PLN', {}, 'pl') }}</span> <span
|
class="badge text-bg-warning text-decoration-line-through flex-fill">{{ product.productStandardPrice | format_currency(product.country.currency, {}, product.country.locale) }}</span> <span
|
||||||
class="badge text-bg-success flex-fill">{{ ((1 - product.priceCurrent / product.productStandardPrice)*100)|number_format(0) }}%</span>
|
class="badge text-bg-success flex-fill">{{ ((1 - product.priceCurrent / product.productStandardPrice)*100)|number_format(0) }}%</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user