Last active
March 5, 2025 07:56
-
-
Save chrisridd/440c0ad64b20d5334335ab8d95e4e747 to your computer and use it in GitHub Desktop.
Compare Kobo store prices
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.