mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-03-01 12:59:36 +00:00
* Fixed Typos and mistranslations in GDPR mode (DSGVO Modus) Fixed Typo enviroment * Create BuerklinProvider based on LCSCProvider * Update GET URLs for Buerklin * Add getToken function analog to Octopart * Remove line break in docs * Remove trailing / in ENDPOINT_URL Use Autowire to use values of environment variables Remove unwanted Code from LCSC-Provider Map json response to DTO variables * Fix variable reference errors ($term → $keyword) Ensure array keys exist before accessing them Optimize API calls to prevent unnecessary requests Improve error handling for better debugging Enhance readability and maintainability of functions * Bumped version v1.16.2 * Update BuerklinProvider.php Change Order of Capabilities * Change order of capabilities in LCSCProvider.php * Change order of capabilities in PollinProvider.php * Try to fix getToken BuerklinProvider.php * Add ip_buerklin_oauth to knpu_oauth2_client.yaml * Update buerklin authorize URL in knpu_oauth2_client.yaml * Update knpu_oauth2_client.yaml * Adapt Buerklin InfoProvider to new Settings mechanism * According to Buerklin API spec it's really 'token' as urlAuthorize endpoint * Rückgabewert ist schon ein Array deshalb kein toArray * Fix API-Access, add image, price and parameter retrieval (Datasheets not yet implemented because it is not available in the API response) * Add Caching of requests, use default query params (language and currency) using a function, Fix Footprint assignment, translate German code comments * Remove DATASHEET from ProviderCapabilities because the Bürklin API doesn't include Datasheet URLs at the moment, more reverse engineering needed * Update BuerklinSettings with existing translatable strings * Improve Buerklin Settings Page * Added Translation strings for Buerklin Info Provider * Improve Buerklin Provider help message * Adapt Buerklin-provider to new settings system * Adapt Buerklin-provider to new settings system: add missing instance of BuerklinSettings * Improve Compliance Parameters parsing * Remove language-dependent RoHs Date code and use shortened ISO format, Add Customs Code without parseValueField * Fix no results for keyword search * Implement BatchInfoProviderInterface for Buerklin Provider * Rename searchBatch to searchByKeywordsBatch to correctly implement BatchInfoProviderInterface * Fix Bulk Info Provider Import for Buerklin * Tranlate comments to English to prepare for Pull-Request * Add phpUnit unit tests for BuerklinProvider * Try fixing PHPStan issues * Remove OAuthTokenManager from BuerklinProviderTest Removed OAuthTokenManager mock from BuerklinProviderTest setup. * Fix Settings must not be instantiated directly * Fix UnitTest for value_typ *edd5fb3319 (r2622249199)Revert "Change order of capabilities in LCSCProvider.php" This reverts commitdfd6f33e52. *edd5fb3319 (r2622249861)Revert "Change order of capabilities in PollinProvider.php" This reverts commitfc2e7265be. * Use language setting for ProductShortURL * Update default language for Buerklin provider to English in documentation * Add suggested improvements from SonarQube * Removed unused use directives * Revert SonarQube proposed change. Having more than one return is acceptable nowadays * Improve getProviderInfo: disable oauth_app_name, add settings_class, improve disabled_help * Implement retrieveROPCToken as proposed in https://github.com/Part-DB/Part-DB-server/pull/1151#discussion_r2622976206 * Add missing ) to retrieveROPCToken * add use OAuthTokenManager and create instance in constructor * Revert the following commits that tried to implement getToken using OAuthTokenManager Revert "add use OAuthTokenManager and create instance in constructor"This reverts commit 2a1e7c9b0974ebd7e082d5a2fa62753d6254a767.Revert "Add missing ) to retrieveROPCToken"This reverts commit8df5cfc49e. Revert "Implement retrieveROPCToken as proposed in https://github.com/Part-DB/Part-DB-server/pull/1151#discussion_r2622976206" This reverts commit66cc732082. * Remove OAuthTokenManager leftovers * Disable buerklin provider if settings fields are empty * Improved docs * Added TODO comment --------- Co-authored-by: root <root@part-db.fritz.box> Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
251 lines
No EOL
9.2 KiB
PHP
251 lines
No EOL
9.2 KiB
PHP
<?php
|
|
/*
|
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
*
|
|
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published
|
|
* by the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
|
|
namespace App\Services\InfoProviderSystem\Providers;
|
|
|
|
use App\Entity\Parts\ManufacturingStatus;
|
|
use App\Entity\Parts\Part;
|
|
use App\Services\InfoProviderSystem\DTOs\FileDTO;
|
|
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
|
|
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
|
|
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
|
|
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
|
|
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
|
use App\Settings\InfoProviderSystem\PollinSettings;
|
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
|
use Symfony\Component\DomCrawler\Crawler;
|
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
|
|
|
class PollinProvider implements InfoProviderInterface
|
|
{
|
|
|
|
public function __construct(private readonly HttpClientInterface $client,
|
|
private readonly PollinSettings $settings,
|
|
)
|
|
{
|
|
}
|
|
|
|
public function getProviderInfo(): array
|
|
{
|
|
return [
|
|
'name' => 'Pollin',
|
|
'description' => 'Webscraping from pollin.de to get part information',
|
|
'url' => 'https://www.pollin.de/',
|
|
'disabled_help' => 'Enable the provider in provider settings',
|
|
'settings_class' => PollinSettings::class,
|
|
];
|
|
}
|
|
|
|
public function getProviderKey(): string
|
|
{
|
|
return 'pollin';
|
|
}
|
|
|
|
public function isActive(): bool
|
|
{
|
|
return $this->settings->enabled;
|
|
}
|
|
|
|
public function searchByKeyword(string $keyword): array
|
|
{
|
|
$response = $this->client->request('GET', 'https://www.pollin.de/search', [
|
|
'query' => [
|
|
'search' => $keyword
|
|
]
|
|
]);
|
|
|
|
$content = $response->getContent();
|
|
|
|
//If the response has us redirected to the product page, then just return the single item
|
|
if ($response->getInfo('redirect_count') > 0) {
|
|
return [$this->parseProductPage($content)];
|
|
}
|
|
|
|
$dom = new Crawler($content);
|
|
|
|
$results = [];
|
|
|
|
//Iterate over each div.product-box
|
|
$dom->filter('div.product-box')->each(function (Crawler $node) use (&$results) {
|
|
$results[] = new SearchResultDTO(
|
|
provider_key: $this->getProviderKey(),
|
|
provider_id: $node->filter('meta[itemprop="productID"]')->attr('content'),
|
|
name: $node->filter('a.product-name')->text(),
|
|
description: '',
|
|
preview_image_url: $node->filter('img.product-image')->attr('src'),
|
|
manufacturing_status: $this->mapAvailability($node->filter('link[itemprop="availability"]')->attr('href')),
|
|
provider_url: $node->filter('a.product-name')->attr('href')
|
|
);
|
|
});
|
|
|
|
return $results;
|
|
}
|
|
|
|
private function mapAvailability(string $availabilityURI): ManufacturingStatus
|
|
{
|
|
return match( $availabilityURI) {
|
|
'http://schema.org/InStock' => ManufacturingStatus::ACTIVE,
|
|
'http://schema.org/OutOfStock' => ManufacturingStatus::DISCONTINUED,
|
|
default => ManufacturingStatus::NOT_SET
|
|
};
|
|
}
|
|
|
|
public function getDetails(string $id): PartDetailDTO
|
|
{
|
|
//Ensure that $id is numeric
|
|
if (!is_numeric($id)) {
|
|
throw new \InvalidArgumentException("The id must be numeric!");
|
|
}
|
|
|
|
$response = $this->client->request('GET', 'https://www.pollin.de/search', [
|
|
'query' => [
|
|
'search' => $id
|
|
]
|
|
]);
|
|
|
|
//The response must have us redirected to the product page
|
|
if ($response->getInfo('redirect_count') > 0) {
|
|
throw new \RuntimeException("Could not resolve the product page for the given id!");
|
|
}
|
|
|
|
$content = $response->getContent();
|
|
|
|
return $this->parseProductPage($content);
|
|
}
|
|
|
|
private function parseProductPage(string $content): PartDetailDTO
|
|
{
|
|
$dom = new Crawler($content);
|
|
|
|
$productPageUrl = $dom->filter('meta[property="product:product_link"]')->attr('content');
|
|
$orderId = trim($dom->filter('span[itemprop="sku"]')->text()); //Text is important here
|
|
|
|
//Calculate the mass
|
|
$massStr = $dom->filter('meta[itemprop="weight"]')->attr('content');
|
|
//Remove the unit
|
|
$massStr = str_replace('kg', '', $massStr);
|
|
//Convert to float and convert to grams
|
|
$mass = (float) $massStr * 1000;
|
|
|
|
//Parse purchase info
|
|
$purchaseInfo = new PurchaseInfoDTO('Pollin', $orderId, $this->parsePrices($dom), $productPageUrl);
|
|
|
|
return new PartDetailDTO(
|
|
provider_key: $this->getProviderKey(),
|
|
provider_id: $orderId,
|
|
name: trim($dom->filter('meta[property="og:title"]')->attr('content')),
|
|
description: $dom->filter('meta[property="og:description"]')->attr('content'),
|
|
category: $this->parseCategory($dom),
|
|
manufacturer: $dom->filter('meta[property="product:brand"]')->count() > 0 ? $dom->filter('meta[property="product:brand"]')->attr('content') : null,
|
|
preview_image_url: $dom->filter('meta[property="og:image"]')->attr('content'),
|
|
//TODO: Find another way to determine the manufacturing status, as the itemprop="availability" is often is not existing anymore in the page
|
|
//manufacturing_status: $this->mapAvailability($dom->filter('link[itemprop="availability"]')->attr('href')),
|
|
provider_url: $productPageUrl,
|
|
notes: $this->parseNotes($dom),
|
|
datasheets: $this->parseDatasheets($dom),
|
|
parameters: $this->parseParameters($dom),
|
|
vendor_infos: [$purchaseInfo],
|
|
mass: $mass,
|
|
);
|
|
}
|
|
|
|
private function parseDatasheets(Crawler $dom): array
|
|
{
|
|
//Iterate over each a element withing div.pol-product-detail-download-files
|
|
$datasheets = [];
|
|
$dom->filter('div.pol-product-detail-download-files a')->each(function (Crawler $node) use (&$datasheets) {
|
|
$datasheets[] = new FileDTO($node->attr('href'), $node->text());
|
|
});
|
|
|
|
return $datasheets;
|
|
}
|
|
|
|
private function parseParameters(Crawler $dom): array
|
|
{
|
|
$parameters = [];
|
|
|
|
//Iterate over each tr.properties-row inside table.product-detail-properties-table
|
|
$dom->filter('table.product-detail-properties-table tr.properties-row')->each(function (Crawler $node) use (&$parameters) {
|
|
$parameters[] = ParameterDTO::parseValueIncludingUnit(
|
|
name: rtrim($node->filter('th.properties-label')->text(), ':'),
|
|
value: trim($node->filter('td.properties-value')->text())
|
|
);
|
|
});
|
|
|
|
return $parameters;
|
|
}
|
|
|
|
private function parseCategory(Crawler $dom): string
|
|
{
|
|
$category = '';
|
|
|
|
//Iterate over each li.breadcrumb-item inside ol.breadcrumb
|
|
$dom->filter('ol.breadcrumb li.breadcrumb-item')->each(function (Crawler $node) use (&$category) {
|
|
//Skip if it has breadcrumb-item-home class
|
|
if (str_contains($node->attr('class'), 'breadcrumb-item-home')) {
|
|
return;
|
|
}
|
|
|
|
|
|
$category .= $node->text() . ' -> ';
|
|
});
|
|
|
|
//Remove the last ' -> '
|
|
return substr($category, 0, -4);
|
|
}
|
|
|
|
private function parseNotes(Crawler $dom): string
|
|
{
|
|
//Concat product highlights and product description
|
|
return $dom->filter('div.product-detail-top-features')->html('') . '<br><br>' . $dom->filter('div.product-detail-description-text')->html('');
|
|
}
|
|
|
|
private function parsePrices(Crawler $dom): array
|
|
{
|
|
//TODO: Properly handle multiple prices, for now we just look at the price for one piece
|
|
|
|
//We assume the currency is always the same
|
|
$currency = $dom->filter('meta[property="product:price:currency"]')->attr('content');
|
|
|
|
//If there is meta[property=highPrice] then use this as the price
|
|
if ($dom->filter('meta[itemprop="highPrice"]')->count() > 0) {
|
|
$price = $dom->filter('meta[itemprop="highPrice"]')->attr('content');
|
|
} else {
|
|
$price = $dom->filter('meta[property="product:price:amount"]')->attr('content');
|
|
}
|
|
|
|
return [
|
|
new PriceDTO(1.0, $price, $currency)
|
|
];
|
|
}
|
|
|
|
public function getCapabilities(): array
|
|
{
|
|
return [
|
|
ProviderCapabilities::BASIC,
|
|
ProviderCapabilities::PICTURE,
|
|
ProviderCapabilities::PRICE,
|
|
ProviderCapabilities::DATASHEET
|
|
];
|
|
}
|
|
} |