Skip to content

Instantly share code, notes, and snippets.

@chrisridd
Last active February 8, 2024 16:38
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chrisridd/440c0ad64b20d5334335ab8d95e4e747 to your computer and use it in GitHub Desktop.
Save chrisridd/440c0ad64b20d5334335ab8d95e4e747 to your computer and use it in GitHub Desktop.
Compare Kobo store prices
#!/usr/bin/perl -w
# Compare book prices across multiple Kobo stores
#
use strict;
use warnings;
use threads;
use WWW::Mechanize;
# FIXME
my $mech = WWW::Mechanize->new(
ssl_opts => { verify_hostname => 0 }
);
my %stores = (
"Austria" => "https://www.kobo.com/at/de/ebook/",
"Australia" => "https://www.kobo.com/au/en/ebook/",
"Belgium (French)" => "https://www.kobo.com/be/fr/ebook/",
"Belgium (Dutch)" => "https://www.kobo.com/be/nl/ebook/",
"Brazil" => "https://www.kobo.com/br/pt/ebook/",
"Canada (English)" => "https://www.kobo.com/ca/en/ebook/",
"Canada (French)" => "https://www.kobo.com/ca/fr/ebook/",
"Switzerland (French)" => "https://www.kobo.com/ch/fr/ebook/",
"Germany" => "https://www.kobo.com/de/de/ebook/",
"Denmark" => "https://www.kobo.com/dk/da/ebook/",
"Spain" => "https://www.kobo.com/es/es/ebook/",
"Finland" => "https://www.kobo.com/fi/fi/ebook/",
"France" => "https://www.kobo.com/fr/fr/ebook/",
"UK" => "https://www.kobo.com/gb/en/ebook/",
"Greece" => "https://www.kobo.com/gr/en/ebook/",
"Hong Kong (English)" => "https://www.kobo.com/hk/en/ebook/",
"Hong Kong (Chinese)" => "https://www.kobo.com/hk/zh/ebook/",
"Eire" => "https://www.kobo.com/ie/en/ebook/",
"India" => "https://www.kobo.com/in/en/ebook/",
"Italy" => "https://www.kobo.com/it/it/ebook/",
"Japan" => "https://www.kobo.com/jp/ja/ebook/",
"Luxembourg" => "https://www.kobo.com/lu/fr/ebook/",
"Mexico" => "https://www.kobo.com/mx/es/ebook/",
"Malaysia" => "https://www.kobo.com/my/en/ebook/",
"Holland" => "https://www.kobo.com/nl/nl/ebook/",
"Norway" => "https://www.kobo.com/no/nb/ebook/",
"New Zealand" => "https://www.kobo.com/nz/en/ebook/",
"Poland" => "https://www.kobo.com/pl/pl/ebook/",
"Portugal" => "https://www.kobo.com/pt/pt/ebook/",
"Sweden" => "https://www.kobo.com/se/sv/ebook/",
"Singapore" => "https://www.kobo.com/sg/en/ebook/",
"Turkey" => "https://www.kobo.com/tr/tr/ebook/",
"Taiwan" => "https://www.kobo.com/tw/zh/ebook/",
"USA" => "https://www.kobo.com/us/en/ebook/",
"World-wide" => "https://www.kobo.com/ww/en/ebook/",
"South Africa" => "https://www.kobo.com/za/en/ebook/",
"Philippines" => "https://www.kobo.com/ph/en/ebook/",
"Thailand" => "https://www.kobo.com/th/en/ebook/",
"Cyprus" => "https://www.kobo.com/cy/en/ebook/",
"Czech Republic" => "https://www.kobo.com/cz/cs/ebook/",
"Estonia" => "https://www.kobo.com/ee/en/ebook/",
"Lithuania" => "https://www.kobo.com/lt/en/ebook/",
"Malta" => "https://www.kobo.com/mt/en/ebook/",
"Romania" => "https://www.kobo.com/ro/ro/ebook/",
"Slovak Republic" => "https://www.kobo.com/sk/en/ebook/",
"Slovenia" => "https://www.kobo.com/si/en/ebook/",
);
# Currency rates as of late-ish 2023. Should fetch these automatically...
my %rates = (
"GBP" => 1.0,
"USD" => 0.79402,
"CAD" => 0.583979993380163,
"EUR" => 0.859160264040688,
"AUD" => 0.50942512511228,
"INR" => 0.009615580263062,
"BRL" => 0.163208031369973,
"DKK" => 0.115310137956743,
"HKD" => 0.101322338276436,
"JPY" => 0.00542642431523,
"MXN" => 0.047523831555867,
"NOK" => 0.074087672866888,
"PLN" => 0.192100811907984,
"SEK" => 0.072038674834173,
"CHF" => 0.899686520376176,
"TWD" => 0.024918649487445, # New Taiwan Dollar
"TRY" => 0.029931861113694,
"CZK" => 0.035607803812372,
"NZD" => 0.469622719489874,
"RON" => 0.174101094205896,
"ZAR" => 0.042517865896064,
"PHP" => 0.01402610689716,
"MYR" => 0.168817987152034,
"SGD" => 0.586037401138296,
);
my $book = shift;
# Allow parameter to be a complete URL - we just want the trailing part of the path
$book =~ s{.*/}{};
# Thread method to fetch the price and currency for a country at a URL
sub get_price {
my $country = shift;
my $url = shift;
$mech->get($url);
my $content = $mech->content;
my $price;
if ($content =~ m{<meta.*?property="og:price".*?content="(.*?)".*?>}m) {
$price = $1;
$price =~ s/,/./;
}
my $currency;
if ($content =~ m{<meta.*?property="og:currency_code".*?content="(.*?)".*?>}m) {
$currency = $1;
}
return ($country, $price, $currency);
}
my @threads;
# Run one thread to check each store. This is simple but a bit excessive
foreach my $country (sort keys %stores) {
push @threads, threads->create(\&get_price, $country, $stores{$country} . $book);
}
my $cheapestPrice = undef;
my @cheapestStore;
my $longestCountry = 0;
my @results;
foreach my $thread (@threads) {
my ($country, $price, $currency) = $thread->join();
$longestCountry = length($country) if length($country) > $longestCountry;
if (defined $price && defined $currency) {
if (exists $rates{$currency}) {
my $gbp = $rates{$currency} * $price;
push @results, [ $country, $currency, $price, $gbp ];
if ((defined $cheapestPrice && $gbp < $cheapestPrice)
|| !defined $cheapestPrice) {
$cheapestPrice = $gbp;
@cheapestStore = ();
}
if ($gbp <= $cheapestPrice) {
push @cheapestStore, $country;
}
} else {
print STDERR "Unknown rate for $currency ($country)\n";
}
}
}
foreach my $aref (sort {$a->[3] <=> $b->[3]} @results) {
printf("%-*s £%.2f %3s %s\n", $longestCountry, $aref->[0], $aref->[3], $aref->[1], $aref->[2]);
}
printf("\nCheapest price is £%.2f in:\n", $cheapestPrice);
foreach my $c (@cheapestStore) {
printf("\t%s %s\n", $c, $stores{$c} . $book);
}
@chrisridd
Copy link
Author

Now fetches the store results in multiple threads, so is much much faster.

The output formatting is slightly cleaner and includes the store URLs at the end for convenience.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment