[Flaschenpost] Add bridge (#2808)

This commit is contained in:
sal0max 2022-07-04 07:29:22 +02:00 committed by GitHub
parent 4f75591060
commit 7d941c2898
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -0,0 +1,364 @@
<?php
class FlaschenpostBridge extends BridgeAbstract
{
const NAME = 'Flaschenpost Bridge';
const URI = 'https://www.flaschenpost.de/';
const DESCRIPTION = 'Aktuelle Angebote auf Flaschenpost.de';
const MAINTAINER = 'sal0max';
const CACHE_TIMEOUT = 3600; // 1 hour
const PARAMETERS = [
[
'zip-code' => [
'name' => 'Postleitzahl',
'type' => 'text',
'required' => true,
'exampleValue' => '80333',
// https://stackoverflow.com/a/7926743/421140
'pattern' => '^(?!01000|99999)(0[1-9]\d{3}|[1-9]\d{4})$',
],
'water' => [
'name' => 'Wasser',
'type' => 'checkbox',
],
'beer' => [
'name' => 'Bier',
'type' => 'checkbox',
],
'lemonade' => [
'name' => 'Limonade',
'type' => 'checkbox',
],
'juice' => [
'name' => 'Saft & Schorle',
'type' => 'checkbox',
],
'wine' => [
'name' => 'Wein & Mehr',
'type' => 'checkbox',
],
'liquor' => [
'name' => 'Spirituosen',
'type' => 'checkbox',
],
'food' => [
'name' => 'Lebensmittel',
'type' => 'checkbox',
],
'household' => [
'name' => 'Haushalt',
'type' => 'checkbox',
],
]
];
public function collectData()
{
$categories = [];
if ($this->getInput('water')) {
array_push(
$categories,
'wasser/spritzig',
'wasser/medium',
'wasser/still',
'wasser/aromatisiert',
'wasser/heilwasser',
'wasser/bio-wasser',
'wasser/gourmet'
);
}
if ($this->getInput('beer')) {
array_push(
$categories,
'bier/alkoholfrei',
'bier/biermischgetraenke',
'bier/craft-beer',
'bier/export-lager-maerzen',
'bier/helles',
'bier/internationale-biere',
'bier/koelsch',
'bier/land-kellerbier',
'bier/malzbier',
'bier/pils',
'bier/radler',
'bier/spezialitaeten',
'bier/weizen-weissbier'
);
}
if ($this->getInput('lemonade')) {
array_push(
$categories,
'limonade/cola',
'limonade/orangenlimonade',
'limonade/zitronenlimonade',
'limonade/cola-mix',
'limonade/teegetraenke',
'limonade/fassbrause',
'limonade/mate',
'limonade/bio',
'limonade/zum-mixen',
'limonade/sonstige-limos'
);
}
if ($this->getInput('juice')) {
array_push(
$categories,
'saft-und-schorle/apfelsaft',
'saft-und-schorle/apfelschorle',
'saft-und-schorle/orangensaft',
'saft-und-schorle/multivitaminsaft',
'saft-und-schorle/maracujasaft',
'saft-und-schorle/traubensaft',
'saft-und-schorle/johannisbeersaft',
'saft-und-schorle/rhabarbersaft',
'saft-und-schorle/rhabarberschorle',
'saft-und-schorle/kirschsaft',
'saft-und-schorle/sonstige-saefte',
'saft-und-schorle/sonstige-schorlen'
);
}
if ($this->getInput('wine')) {
array_push(
$categories,
'wein-und-mehr/weisswein',
'wein-und-mehr/rotwein',
'wein-und-mehr/rose',
'wein-und-mehr/bio-wein',
'wein-und-mehr/sonstige-weine',
'wein-und-mehr/sekt-mehr',
'wein-und-mehr/probierpakete',
'wein-und-mehr/gluehwein'
);
}
if ($this->getInput('liquor')) {
array_push(
$categories,
'spirituosen/wodka',
'spirituosen/gin',
'spirituosen/whisky',
'spirituosen/rum',
'spirituosen/weitere-spirituosen',
'spirituosen/kraeuterlikoer',
'spirituosen/weitere-likoere',
'spirituosen/aperitif'
);
}
if ($this->getInput('food')) {
array_push(
$categories,
'lebensmittel/veggie-vegan',
'lebensmittel/kaffee-tee',
'lebensmittel/milch-alternativen',
'lebensmittel/tiefkuehltruhe',
'lebensmittel/nuesse-trockenobst',
'lebensmittel/suesses-salziges',
'lebensmittel/nudeln-reis-getreide',
'lebensmittel/fertiges-konserven',
'lebensmittel/sossen-oele-gewuerze'
);
}
if ($this->getInput('household')) {
array_push(
$categories,
'haushalt/hygieneartikel',
'haushalt/gesundheit-verhuetung',
'haushalt/kueche',
'haushalt/haushaltsartikel',
'haushalt/spuelen-reinigen',
'haushalt/waschen'
);
}
foreach ($categories as $category) {
try {
$url = sprintf('https://www.flaschenpost.de/%s?plz=%s', $category, $this->getInput('zip-code'));
// Gives redirect on unknown zip-code
$html = getSimpleHTMLDOM($url, [], [CURLOPT_FOLLOWLOCATION => false]);
} catch (\Exception $e) {
// skip
continue;
}
// extract the JavaScript block which contains all the data we need
$regex = '/(\{childElements:\[.*\})\];/';
preg_match($regex, $html, $matches);
$js = $matches[1];
// convert JavaScript to JSON
$js = $this->jsToJson($js);
// get all products
$json_decode = json_decode($js, false, 512, JSON_THROW_ON_ERROR);
$products = $this->recursiveFind((array) $json_decode, 'products');
foreach ($products as $product) {
// there can be multiple variants, like 0.5l and 0.33l bottles
foreach ($product->product->articles as $article) {
$this->addArticle($article, $product->product);
}
}
}
}
public function getName(): string
{
$categories = [];
if ($this->getInput('water')) {
$categories[] = 'Wasser';
}
if ($this->getInput('beer')) {
$categories[] = 'Bier';
}
if ($this->getInput('lemonade')) {
$categories[] = 'Limonade';
}
if ($this->getInput('juice')) {
$categories[] = 'Saft & Schorle';
}
if ($this->getInput('wine')) {
$categories[] = 'Wein & Mehr';
}
if ($this->getInput('liquor')) {
$categories[] = 'Spirituosen';
}
if ($this->getInput('food')) {
$categories[] = 'Lebensmittel';
}
if ($this->getInput('household')) {
$categories[] = 'Haushalt';
}
if (empty($categories)) {
return $this::NAME;
} else {
return $this::NAME . ' ' . implode(', ', $categories);
}
}
private function jsToJson(string $js): string
{
// remove all html
$js = strip_tags($js);
// escape double quotes
$js = str_replace('"', '\\"', $js);
// add double quotes to all keys
$js = preg_replace('/(?<=[,{])(\w+)(?=:)/', '"$1"', $js);
// replace all single quotes with double quotes at all values
$js = str_replace('\'', '"', $js);
// sometimes, there are more than one JSON blocks; we're interested in the first one
$js = $this->splitJsonObjects($js)[0];
return $js;
}
private function addArticle($article, $product)
{
$regularPrice = $article->trackingDefaultPrice;
$discountPrice = $article->crossedPrice;
$discount = round((($regularPrice - $discountPrice) / $regularPrice) * 100.0);
$regularPriceString = $article->defaultPrice;
$discountPriceString = $article->price;
// only discounted products
if ($regularPrice != $discountPrice) {
$name = str_replace('"', '\'', $product->name);
$imageUrl = 'https://image.flaschenpost.de/cdn-cgi/image/width=120,height=120,q=50/articles/small/'
. $article->articleId . '.png';
$pricePerUnit = str_replace(['(', ')'], '', $article->pricePerUnit);
$deposit = $article->deposit ? "Pfand: $article->deposit" : 'Pfandfrei';
$alcohol = $product->alcoholInfo ? str_replace(['enthält', 'Vol.-', 'Alkohol'], '', $product->alcoholInfo)
. ' Alkohol' : '';
$description = <<<EOD
<div style="padding: 20px; display: flex;">
<img src="{$imageUrl}" alt="{$name}" style="float: left; margin-right: 35px;"/>
<p style="display: inline-block; align-self: center; line-height: 1.5rem;">
$pricePerUnit
<br>
{$article->shortDescription}
<br>
$deposit
<br>
$alcohol
</p>
</div>
EOD;
$item['title'] = "$name: $discountPriceString statt $regularPriceString (-$discount\u{2009}%)";
$item['content'] = $description;
// use current date (@midnight) as timestamp
$item['timestamp'] = (new \DateTime())
->setTimezone(new \DateTimeZone('Europe/Berlin'))
->setTime(0, 0)
->getTimestamp();
$item['uri'] = urljoin(
'https://www.flaschenpost.de/',
"{$product->brandWebShopUrl}/{$product->webShopUrl}"
);
// use "name-<timestamp>" as uid; that way, there's a new entry each day, when a product stays discounted
$item['uid'] = $name . '-' . $item['timestamp'];
// only add if unique
$exists = false;
foreach ($this->items as $i) {
if ($i['uri'] === $item['uri']) {
$exists = true;
break;
}
}
if (!$exists) {
$this->items[] = $item;
}
}
}
public function getIcon()
{
return 'https://image.flaschenpost.de/CI/fp-favicon.png';
}
// https://stackoverflow.com/a/3975706/421140
private function recursiveFind(array $haystack, $needle)
{
$iterator = new \RecursiveArrayIterator($haystack);
$recursive = new \RecursiveIteratorIterator(
$iterator,
\RecursiveIteratorIterator::SELF_FIRST
);
foreach ($recursive as $key => $value) {
if ($key === $needle) {
return $value;
}
}
return null;
}
/**
* http://ryanuber.com/07-31-2012/split-and-decode-json-php.html
*
* json_split_objects - Return an array of many JSON objects
*
* In some applications (such as PHPUnit, or salt), JSON output is presented as multiple
* objects, which you cannot simply pass in to json_decode(). This function will split
* the JSON objects apart and return them as an array of strings, one object per indice.
*
* @param string $json The JSON data to parse
*
* @return array (of strings)
*/
private function splitJsonObjects(string $json): array
{
$q = false;
$len = strlen($json);
for ($l = $c = $i = 0; $i < $len; $i++) {
$json[$i] == '"' && ($i > 0 ? $json[$i - 1] : '') != '\\' && $q = !$q;
if (!$q && in_array($json[$i], [' ', "\r", "\n", "\t"])) {
continue;
}
in_array($json[$i], ['{', '[']) && !$q && $l++;
in_array($json[$i], ['}', ']']) && !$q && $l--;
(isset($objects[$c]) && $objects[$c] .= $json[$i]) || $objects[$c] = $json[$i];
$c += ($l == 0);
}
return $objects ?? [];
}
}