diff --git a/README.md b/README.md
index fc78840..1318d5e 100644
--- a/README.md
+++ b/README.md
@@ -18,8 +18,9 @@ Miscellaneous scripts for different purposes. Mostly unrelated to each other.
| Infosec
Automation | [`make-mac-prefixes.py`](bin/make-mac-prefixes.py)
Python 3 | Processes registered MAC address prefixes from [IEEE MA-L Assignments (CSV)](https://standards.ieee.org/products-programs/regauth/) (stdin) to Nmap's [`nmap-mac-prefixes`](https://github.com/nmap/nmap/blob/master/nmap-mac-prefixes) (stdout) with a few additional unregistered OUIs.
`curl https://standards-oui.ieee.org/oui/oui.csv \| make-mac-prefixes.py > nmap-mac-prefixes` |
| WordPress | [`test-cache-enabler.py`](bin/test-cache-enabler.py)
Python 3 | Tests whether the Cache Enabler by KeyCDN (WordPress) is working properly on the URLs given as arguments.
`test-cache-enabler.py https://example.com [...]` |
| Web | [`detect-modified-html-element.sh`](bin/detect-modified-html-element.sh)
Shell (bash) | Checks HTML element changes on a web page since last run. Configured via environment variables.
Recommended to be executed as a SystemD [service](systemd/detect-modified-html-element.service.example). |
+| Web | [`product-pricelimiter.sh`](bin/product-pricelimiter.sh)
Shell (bash) | Compare product price on a web page with a given maximum price. Use, e.g., developer tools on your browser to find the HTML element containing the price.
`product-pricelimiter.sh ProductURL Element MaxPrice` |
| Web | `koronarokotusaika.sh`
Shell (bash) | This script has been removed as koronarokotusaika.fi (bookcovidvaccine.fi) has been shut down on April 28, 2023. |
-| Web | [`xxl-product-pricelimiter.sh`](bin/xxl-product-pricelimiter.sh)
Shell (bash) | XXL.fi product price checker / limiter.
`xxl-product-pricelimiter.sh XXL.fi-ProductURL MaxPrice` |
+| Web | `xxl-product-pricelimiter.sh`
Shell (bash) | This script has been replaced by the generalized [`product-pricelimiter.sh`](bin/product-pricelimiter.sh) as the HTML structure of XXL.fi has changed. |
## Scripts that require `sudo` privileges ([`sbin/`](sbin/))
diff --git a/bin/product-pricelimiter.sh b/bin/product-pricelimiter.sh
new file mode 100755
index 0000000..0ca8cfc
--- /dev/null
+++ b/bin/product-pricelimiter.sh
@@ -0,0 +1,118 @@
+#!/bin/bash
+read -r -d '' USAGE << EOM
+# ------------------------------------------------------------------------------
+# Compare product price on a web page with a given maximum price.
+#
+# Usage: product-pricelimiter.sh ProductURL Element MaxPrice
+#
+# ProductURL web page URL to fetch the current price from
+# Element the HTML element containing the price (#id or .class)
+# MaxPrice float number
+#
+# Exit codes:
+#
+# 0 OK Price is found and lower than or equal with the MaxPrice.
+# 1 ERROR An error has occured; unable to tell the result.
+# 2 WAIT Price is found but higher than the MaxPrice.
+#
+# Author : Esa Jokinen (oh2fih)
+# Home : https://github.com/oh2fih/Misc-Scripts
+# ------------------------------------------------------------------------------
+EOM
+
+# Fake user-agent for curl
+
+UA="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
+UA+="(KHTML, like Gecko) Chrome/127.0.0.0 Safari/537.36"
+
+# Test the inputs...
+
+if [ "$#" -ne 3 ]; then
+ echo -e "\033[0;33m${USAGE}\033[0m" >&2
+ exit 1
+fi
+
+if [[ "$3" =~ ^[+-]?[0-9]+[\.,]?[0-9]*$ ]] 2>/dev/null; then
+ producturl="$1"
+ selector="$2"
+ maxprice=$(printf "%s" "$3" | sed 's/,/./g')
+else
+ echo -e "\033[0;31mMax price should be a (float) number!\033[0m" >&2
+ exit 1
+fi
+
+# Validate the URL
+
+regex='(https?)://[-A-Za-z0-9\+&@#/%?=~_|!:,.;]*[-A-Za-z0-9\+&@#/%=~_|]'
+if ! [[ $producturl =~ $regex ]]; then
+ echo -e "\033[0;31mThe first argument was not a valid URL!\033[0m" >&2
+ exit 1
+fi
+
+# Check for requirements. Print all unmet requirements at once.
+
+required_command() {
+ if ! command -v "$1" &> /dev/null; then
+ if [ -z ${2+x} ]; then
+ echo -e "\033[0;31mThis script requires ${1}!\033[0m" >&2
+ else
+ echo -e "\033[0;31mThis script requires ${1} ${2}!\033[0m" >&2
+ fi
+ ((UNMET=UNMET+1))
+ fi
+}
+
+UNMET=0
+
+required_command "hxselect" "; Please install html-xml-utils"
+required_command "hxnormalize" "; Please install html-xml-utils"
+required_command "grep" "for parsing the page content"
+required_command "sed" "for converting delimiters"
+required_command "curl" "for fetching the web page"
+required_command "sort" "for comparing float numbers"
+required_command "head" "for comparing float numbers"
+
+if [ "$UNMET" -gt 0 ]; then
+ exit 1
+fi
+
+# Normalize the web page and get the monitored element
+
+curl_failure() {
+ echo -e "\033[0;31mFailed to fetch ${producturl}\033[0m" >&2
+ exit 1
+}
+
+element_contents=$(
+ curl -A "$UA" -s "$producturl" \
+ | hxnormalize -x \
+ | hxselect -c "$selector"
+ )
+if [ "$element_contents" == "" ]; then
+ echo -e "\033[0;31mFailed to fetch \"$selector\" in ${producturl}\033[0m" >&2
+ exit 1
+fi
+
+# Extract price (first match) from the element and compare it with the limit
+
+price=$(
+ echo "$element_contents" \
+ | grep -m 1 -Eo '[0-9]+([,\.][0-9]{0,2})?' \
+ | sed 's/,/./g' | head -n 1
+ )
+if [ "$price" == "" ]; then
+ echo -e "\033[0;31mPrice not found from \"${selector}\"!\033[0m" >&2
+ exit 1
+fi
+
+lower=$(printf "%s\n%s" "$price" "$maxprice" | sort -g | head -1)
+
+if [ "$lower" = "$price" ]; then
+ echo -ne "\033[0;32mGood to buy! "
+ echo -e "The price in \"$selector\" is now $price €\033[0m"
+ exit 0
+else
+ echo -ne "\033[0;33mPlease be patient! "
+ echo -e "The price in \"$selector\" is still $price €\033[0m"
+ exit 2
+fi
diff --git a/bin/xxl-product-pricelimiter.sh b/bin/xxl-product-pricelimiter.sh
deleted file mode 100755
index aff8197..0000000
--- a/bin/xxl-product-pricelimiter.sh
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/bin/bash
-# ------------------------------------------------------------------------------
-# Gets the current product price from a www.xxl.fi product page, compares it
-# with a maximum price given and exits with error level 0 if the price is lower
-# than the maximum price.
-#
-# Usage: xxl-product-pricelimiter.sh ProductURL MaxPrice
-#
-# Author : Esa Jokinen (oh2fih)
-# Home : https://github.com/oh2fih/Misc-Scripts
-# ------------------------------------------------------------------------------
-
-# Test the inputs...
-if [ "$#" -ne 2 ]; then
- printf "\n\e[33m%s\e[0m\n\n" "Usage: $0 XXL.fi-ProductURL MaxPrice" >&2
- exit 1
-fi
-
-if [[ "$2" =~ ^[+-]?[0-9]+[\.,]?[0-9]*$ ]] 2>/dev/null; then
- producturl="$1"
- maxprice=$(printf "%s" "$2"| sed 's/,/./g')
-else
- printf "\n\e[31m%s\e[0m\n\n" "Max price should be a (float) number!" >&2
- exit 1
-fi
-
-# Validate the URL
-regex='(https?)://[-A-Za-z0-9\+&@#/%?=~_|!:,.;]*[-A-Za-z0-9\+&@#/%=~_|]'
-if [[ $producturl =~ $regex ]]; then
-
- # Download the page and get current price.
- productpage=$(curl --silent "$producturl")
-
- productname=$(
- printf "%s" "$productpage" \
- | grep "data\-product\-name" \
- | head -n 1 \
- | grep -o '".*"' \
- | sed 's/"//g'
- )
-
- productprice=$(
- printf "%s" "$productpage" \
- | grep "data\-product\-price" \
- | grep -o '".*"' \
- | sed -e 's/[^0-9,\.]*//g' \
- | sed 's/,/./g'
- )
-
- if [ -z "$productprice" ]; then
- printf "\n\e[31m%s\e[0m\n\n" "Unable to capture product price." >&2
- exit 1
- fi
-
- lower=$(printf "%s\n%s" "$productprice" "$maxprice" | sort -g | head -1)
-
- # Compare the prices.
- if [ "$lower" = "$productprice" ]; then
- printf "\n\e[32m%s\e[0m" "Good to buy! The price of "
- printf "\e[32m%s\e[0m\n\n" "\"$productname\" is now $productprice €"
- exit 0
- else
- printf "\n\e[33m%s\e[0m" "Please be patient! The price of " >&2
- printf "\e[33m%s\e[0m\n\n" "\"$productname\" is still $productprice €" >&2
- exit 1
- fi
-
-else
-
- printf "\n\e[31m%s\e[0m\n\n" "The first argument was not a valid URL." >&2
- exit 1
-
-fi