| line | stmt | bran | cond | sub | pod | time | code | 
| 1 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 2 |  |  |  |  |  |  | package App::BarnesNoble::WishListMinder; | 
| 3 |  |  |  |  |  |  | # | 
| 4 |  |  |  |  |  |  | # Copyright 2014 Christopher J. Madsen | 
| 5 |  |  |  |  |  |  | # | 
| 6 |  |  |  |  |  |  | # Author: Christopher J. Madsen | 
| 7 |  |  |  |  |  |  | # Created: 15 Jun 2014 | 
| 8 |  |  |  |  |  |  | # | 
| 9 |  |  |  |  |  |  | # This program is free software; you can redistribute it and/or modify | 
| 10 |  |  |  |  |  |  | # it under the same terms as Perl itself. | 
| 11 |  |  |  |  |  |  | # | 
| 12 |  |  |  |  |  |  | # This program is distributed in the hope that it will be useful, | 
| 13 |  |  |  |  |  |  | # but WITHOUT ANY WARRANTY; without even the implied warranty of | 
| 14 |  |  |  |  |  |  | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See either the | 
| 15 |  |  |  |  |  |  | # GNU General Public License or the Artistic License for more details. | 
| 16 |  |  |  |  |  |  | # | 
| 17 |  |  |  |  |  |  | # ABSTRACT: Monitor a Barnes & Noble wishlist for price changes | 
| 18 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 19 |  |  |  |  |  |  |  | 
| 20 | 1 |  |  | 1 |  | 15793 | use 5.010; | 
|  | 1 |  |  |  |  | 3 |  | 
|  | 1 |  |  |  |  | 31 |  | 
| 21 | 1 |  |  | 1 |  | 3 | use strict; | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 25 |  | 
| 22 | 1 |  |  | 1 |  | 3 | use warnings; | 
|  | 1 |  |  |  |  | 1 |  | 
|  | 1 |  |  |  |  | 42 |  | 
| 23 |  |  |  |  |  |  |  | 
| 24 |  |  |  |  |  |  | our $VERSION = '0.002'; | 
| 25 |  |  |  |  |  |  | # This file is part of App-BarnesNoble-WishListMinder 0.002 (October 18, 2014) | 
| 26 |  |  |  |  |  |  |  | 
| 27 | 1 |  |  | 1 |  | 700 | use Path::Tiny; | 
|  | 1 |  |  |  |  | 12338 |  | 
|  | 1 |  |  |  |  | 52 |  | 
| 28 |  |  |  |  |  |  | #use Smart::Comments; | 
| 29 |  |  |  |  |  |  |  | 
| 30 | 1 |  |  | 1 |  | 504 | use Moo; | 
|  | 1 |  |  |  |  | 11074 |  | 
|  | 1 |  |  |  |  | 6 |  | 
| 31 | 1 |  |  | 1 |  | 1747 | use namespace::clean; | 
|  | 1 |  |  |  |  | 9121 |  | 
|  | 1 |  |  |  |  | 5 |  | 
| 32 |  |  |  |  |  |  |  | 
| 33 |  |  |  |  |  |  | # Like == but undef equals only itself | 
| 34 |  |  |  |  |  |  | sub _numEq | 
| 35 |  |  |  |  |  |  | { | 
| 36 | 0 |  |  | 0 |  |  | my ($one, $two) = @_; | 
| 37 |  |  |  |  |  |  |  | 
| 38 | 0 | 0 | 0 |  |  |  | return !1 if (defined($one) xor defined($two)); | 
| 39 | 0 | 0 |  |  |  |  | return 1 unless defined $one; | 
| 40 | 0 |  |  |  |  |  | $one == $two; | 
| 41 |  |  |  |  |  |  | } # end _numEq | 
| 42 |  |  |  |  |  |  |  | 
| 43 |  |  |  |  |  |  | # Like eq but undef equals only itself | 
| 44 |  |  |  |  |  |  | sub _eq | 
| 45 |  |  |  |  |  |  | { | 
| 46 | 0 |  |  | 0 |  |  | my ($one, $two) = @_; | 
| 47 |  |  |  |  |  |  |  | 
| 48 | 0 | 0 | 0 |  |  |  | return !1 if (defined($one) xor defined($two)); | 
| 49 | 0 | 0 |  |  |  |  | return 1 unless defined $one; | 
| 50 | 0 |  |  |  |  |  | $one eq $two; | 
| 51 |  |  |  |  |  |  | } # end _numEq | 
| 52 |  |  |  |  |  |  |  | 
| 53 |  |  |  |  |  |  | sub _format_price { | 
| 54 | 0 |  |  | 0 |  |  | my $price = shift; | 
| 55 | 0 | 0 |  |  |  |  | if (defined $price) { | 
| 56 | 0 |  |  |  |  |  | $price = sprintf '$%03d', $price; | 
| 57 | 0 |  |  |  |  |  | substr($price, -2, 0, '.'); | 
| 58 | 0 |  |  |  |  |  | $price; | 
| 59 |  |  |  |  |  |  | } else { | 
| 60 | 0 |  |  |  |  |  | 'unavailable'; | 
| 61 |  |  |  |  |  |  | } | 
| 62 |  |  |  |  |  |  | } # end _format_price | 
| 63 |  |  |  |  |  |  |  | 
| 64 |  |  |  |  |  |  | sub _format_timestamp { | 
| 65 | 0 |  |  | 0 |  |  | require Time::Piece; | 
| 66 |  |  |  |  |  |  |  | 
| 67 | 0 |  |  |  |  |  | Time::Piece->gmtime(shift)->strftime("%Y-%m-%d %H:%M:%S"); | 
| 68 |  |  |  |  |  |  | } | 
| 69 |  |  |  |  |  |  |  | 
| 70 |  |  |  |  |  |  | #===================================================================== | 
| 71 |  |  |  |  |  |  |  | 
| 72 |  |  |  |  |  |  | has mech => qw(is lazy); | 
| 73 |  |  |  |  |  |  | sub _build_mech { | 
| 74 | 0 |  |  | 0 |  |  | require WWW::Mechanize; | 
| 75 | 0 |  |  |  |  |  | WWW::Mechanize->new( | 
| 76 |  |  |  |  |  |  | autocheck => 1, | 
| 77 |  |  |  |  |  |  | cookie_jar => { file => shift->cookie_file, autosave => 1 }, | 
| 78 |  |  |  |  |  |  | ); | 
| 79 |  |  |  |  |  |  | } # end _build_mech | 
| 80 |  |  |  |  |  |  |  | 
| 81 |  |  |  |  |  |  | has dir => qw(is lazy); | 
| 82 |  |  |  |  |  |  | sub _build_dir { | 
| 83 | 0 |  |  | 0 |  |  | require File::HomeDir; | 
| 84 | 0 |  |  |  |  |  | File::HomeDir->VERSION(0.93); # my_dist_data | 
| 85 |  |  |  |  |  |  |  | 
| 86 | 0 |  | 0 |  |  |  | path(File::HomeDir->my_dist_data('App-BarnesNoble-WishListMinder', | 
| 87 |  |  |  |  |  |  | { create => 1 }) | 
| 88 |  |  |  |  |  |  | or die "Can't determine data directory"); | 
| 89 |  |  |  |  |  |  | } # end _build_dir | 
| 90 |  |  |  |  |  |  |  | 
| 91 |  |  |  |  |  |  | has config_file => qw(is lazy); | 
| 92 |  |  |  |  |  |  | sub _build_config_file { | 
| 93 | 0 |  |  | 0 |  |  | shift->dir->child('config.ini'); | 
| 94 |  |  |  |  |  |  | } # end _build_config_file | 
| 95 |  |  |  |  |  |  |  | 
| 96 |  |  |  |  |  |  | has config => qw(is lazy); | 
| 97 |  |  |  |  |  |  | sub _build_config { | 
| 98 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 99 | 0 |  |  |  |  |  | require Config::Tiny; | 
| 100 | 0 |  |  |  |  |  | my $fn = $self->config_file; | 
| 101 |  |  |  |  |  |  |  | 
| 102 | 0 | 0 |  |  |  |  | Config::Tiny->read("$fn", 'utf8') | 
| 103 |  |  |  |  |  |  | or die "Unable to read $fn: " . Config::Tiny->errstr; | 
| 104 |  |  |  |  |  |  | } # end _build_config | 
| 105 |  |  |  |  |  |  |  | 
| 106 |  |  |  |  |  |  | has cookie_file => qw(is lazy); | 
| 107 |  |  |  |  |  |  | sub _build_cookie_file { | 
| 108 | 0 |  |  | 0 |  |  | shift->dir->child('cookies.txt'); | 
| 109 |  |  |  |  |  |  | } # end _build_cookie_file | 
| 110 |  |  |  |  |  |  |  | 
| 111 |  |  |  |  |  |  | has db_file => qw(is lazy); | 
| 112 |  |  |  |  |  |  | sub _build_db_file { | 
| 113 | 0 |  |  | 0 |  |  | shift->dir->child('wishlist.sqlite'); | 
| 114 |  |  |  |  |  |  | } # end _build_db_file | 
| 115 |  |  |  |  |  |  |  | 
| 116 |  |  |  |  |  |  | has dbh => qw(is lazy  predicate 1  clearer _clear_dbh); | 
| 117 |  |  |  |  |  |  | sub _build_dbh { | 
| 118 | 0 |  |  | 0 |  |  | my $self = shift; | 
| 119 |  |  |  |  |  |  |  | 
| 120 | 0 |  |  |  |  |  | require DBI; | 
| 121 | 0 |  |  |  |  |  | DBI->VERSION(1.38);           # last_insert_id | 
| 122 |  |  |  |  |  |  |  | 
| 123 | 0 |  |  |  |  |  | my $fn = $self->db_file; | 
| 124 | 0 |  |  |  |  |  | my $exists = $fn->exists; | 
| 125 |  |  |  |  |  |  |  | 
| 126 | 0 |  |  |  |  |  | my $dbh = DBI->connect("dbi:SQLite:dbname=$fn","","", | 
| 127 |  |  |  |  |  |  | { AutoCommit => 0, PrintError => 0, RaiseError => 1, | 
| 128 |  |  |  |  |  |  | sqlite_unicode => 1 }); | 
| 129 |  |  |  |  |  |  |  | 
| 130 | 0 | 0 |  |  |  |  | $self->create_database_schema($dbh) unless $exists; | 
| 131 |  |  |  |  |  |  |  | 
| 132 | 0 |  |  |  |  |  | $dbh; | 
| 133 |  |  |  |  |  |  | } # end _build_dbh | 
| 134 |  |  |  |  |  |  |  | 
| 135 |  |  |  |  |  |  | sub close_dbh | 
| 136 |  |  |  |  |  |  | { | 
| 137 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 138 |  |  |  |  |  |  |  | 
| 139 | 0 | 0 |  |  |  |  | if ($self->has_dbh) { | 
| 140 | 0 |  |  |  |  |  | my $dbh = $self->dbh; | 
| 141 | 0 |  |  |  |  |  | $dbh->rollback; | 
| 142 | 0 |  |  |  |  |  | $dbh->disconnect; | 
| 143 | 0 |  |  |  |  |  | $self->_clear_dbh; | 
| 144 |  |  |  |  |  |  | } | 
| 145 |  |  |  |  |  |  | } # end close_dbh | 
| 146 |  |  |  |  |  |  |  | 
| 147 |  |  |  |  |  |  | has scraper => qw(is lazy); | 
| 148 |  |  |  |  |  |  | sub _build_scraper { | 
| 149 | 0 |  |  | 0 |  |  | require Web::Scraper::BarnesNoble::WishList; | 
| 150 |  |  |  |  |  |  |  | 
| 151 | 0 |  |  |  |  |  | Web::Scraper::BarnesNoble::WishList::bn_scraper(); | 
| 152 |  |  |  |  |  |  | } # end _build_scraper | 
| 153 |  |  |  |  |  |  |  | 
| 154 |  |  |  |  |  |  | has updates => qw(is ro  default) => sub { {} }; | 
| 155 |  |  |  |  |  |  |  | 
| 156 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 157 |  |  |  |  |  |  | sub configure | 
| 158 |  |  |  |  |  |  | { | 
| 159 | 0 |  |  | 0 | 0 |  | my ($self) = @_; | 
| 160 |  |  |  |  |  |  |  | 
| 161 | 0 |  |  |  |  |  | my $config_file = $self->config_file; | 
| 162 |  |  |  |  |  |  |  | 
| 163 | 0 |  |  |  |  |  | say "Your config file is:\n $config_file"; | 
| 164 |  |  |  |  |  |  |  | 
| 165 | 0 | 0 |  |  |  |  | unless ($config_file->is_file) { | 
| 166 | 0 | 0 |  |  |  |  | die "$config_file is a directory!\n" if $config_file->is_dir; | 
| 167 | 0 |  |  |  |  |  | $config_file->spew_utf8(<<'END CONFIG'); | 
| 168 |  |  |  |  |  |  | ;						-*-conf-windows-*- | 
| 169 |  |  |  |  |  |  | ; Your credentials for logging in to the Barnes & Noble website go here: | 
| 170 |  |  |  |  |  |  | email    = YOUR EMAIL HERE | 
| 171 |  |  |  |  |  |  | password = YOUR PASSWORD HERE | 
| 172 |  |  |  |  |  |  |  | 
| 173 |  |  |  |  |  |  | ; If you want the Price Drop Alert emails to go to a different address, | 
| 174 |  |  |  |  |  |  | ; uncomment the next line and set the email address. | 
| 175 |  |  |  |  |  |  | ;report   = EMAIL ADDRESS FOR ALERTS | 
| 176 |  |  |  |  |  |  |  | 
| 177 |  |  |  |  |  |  | ; Next, you need one or more wishlists to monitor. | 
| 178 |  |  |  |  |  |  | ; Each wishlist must have a unique name in [brackets]. | 
| 179 |  |  |  |  |  |  |  | 
| 180 |  |  |  |  |  |  | [My Wishlist] | 
| 181 |  |  |  |  |  |  | wishlist = WISHLIST URL HERE | 
| 182 |  |  |  |  |  |  | END CONFIG | 
| 183 | 0 |  |  |  |  |  | say "\nYou need to replace the ALL CAPS placeholders with the correct values."; | 
| 184 |  |  |  |  |  |  | } | 
| 185 |  |  |  |  |  |  |  | 
| 186 | 0 | 0 | 0 |  |  |  | if (my $editor = $ENV{VISUAL} || $ENV{EDITOR}) { | 
| 187 | 0 |  |  |  |  |  | require Text::ParseWords; | 
| 188 | 0 |  |  |  |  |  | system(Text::ParseWords::shellwords($editor), "$config_file"); | 
| 189 |  |  |  |  |  |  | } | 
| 190 |  |  |  |  |  |  | } # end configure | 
| 191 |  |  |  |  |  |  |  | 
| 192 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 193 |  |  |  |  |  |  | sub create_database_schema | 
| 194 |  |  |  |  |  |  | { | 
| 195 | 0 |  |  | 0 | 0 |  | my ($self, $dbh) = @_; | 
| 196 |  |  |  |  |  |  |  | 
| 197 | 0 |  |  |  |  |  | $dbh->do("PRAGMA foreign_keys = ON"); | 
| 198 |  |  |  |  |  |  |  | 
| 199 | 0 |  |  |  |  |  | $dbh->do(<<''); | 
| 200 |  |  |  |  |  |  | CREATE TABLE books ( | 
| 201 |  |  |  |  |  |  | ean         INTEGER PRIMARY KEY, | 
| 202 |  |  |  |  |  |  | title       TEXT NOT NULL, | 
| 203 |  |  |  |  |  |  | author      TEXT | 
| 204 |  |  |  |  |  |  | ) | 
| 205 |  |  |  |  |  |  |  | 
| 206 | 0 |  |  |  |  |  | $dbh->do(<<''); | 
| 207 |  |  |  |  |  |  | CREATE TABLE wishlists ( | 
| 208 |  |  |  |  |  |  | wishlist_id   INTEGER PRIMARY KEY, | 
| 209 |  |  |  |  |  |  | url           TEXT NOT NULL UNIQUE, | 
| 210 |  |  |  |  |  |  | last_fetched  TIMESTAMP | 
| 211 |  |  |  |  |  |  | ) | 
| 212 |  |  |  |  |  |  |  | 
| 213 | 0 |  |  |  |  |  | $dbh->do(<<''); | 
| 214 |  |  |  |  |  |  | CREATE TABLE wishlist_books ( | 
| 215 |  |  |  |  |  |  | wishlist_id   INTEGER NOT NULL REFERENCES wishlists, | 
| 216 |  |  |  |  |  |  | ean           INTEGER NOT NULL REFERENCES books, | 
| 217 |  |  |  |  |  |  | priority      INTEGER, | 
| 218 |  |  |  |  |  |  | date_added    DATE NOT NULL DEFAULT CURRENT_DATE, | 
| 219 |  |  |  |  |  |  | date_removed  DATE, | 
| 220 |  |  |  |  |  |  | PRIMARY KEY (wishlist_id,ean) | 
| 221 |  |  |  |  |  |  | ) | 
| 222 |  |  |  |  |  |  |  | 
| 223 | 0 |  |  |  |  |  | $dbh->do(<<''); | 
| 224 |  |  |  |  |  |  | CREATE TABLE prices ( | 
| 225 |  |  |  |  |  |  | ean            INTEGER NOT NULL REFERENCES books, | 
| 226 |  |  |  |  |  |  | first_recorded TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, | 
| 227 |  |  |  |  |  |  | last_checked   TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, | 
| 228 |  |  |  |  |  |  | current        TINYINT NOT NULL DEFAULT 1, | 
| 229 |  |  |  |  |  |  | price          INTEGER, | 
| 230 |  |  |  |  |  |  | list_price     INTEGER, | 
| 231 |  |  |  |  |  |  | discount       INTEGER, | 
| 232 |  |  |  |  |  |  | PRIMARY KEY (ean,first_recorded) | 
| 233 |  |  |  |  |  |  | ) | 
| 234 |  |  |  |  |  |  |  | 
| 235 | 0 |  |  |  |  |  | $dbh->commit; | 
| 236 |  |  |  |  |  |  |  | 
| 237 |  |  |  |  |  |  | } # end create_database_schema | 
| 238 |  |  |  |  |  |  |  | 
| 239 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 240 |  |  |  |  |  |  | sub login | 
| 241 |  |  |  |  |  |  | { | 
| 242 | 0 |  |  | 0 | 0 |  | my ($self) = shift; | 
| 243 |  |  |  |  |  |  |  | 
| 244 | 0 |  |  |  |  |  | my ($config, $m) = ($self->config->{_}, $self->mech); | 
| 245 |  |  |  |  |  |  |  | 
| 246 | 0 |  |  |  |  |  | $m->get('https://www.barnesandnoble.com/signin'); | 
| 247 |  |  |  |  |  |  |  | 
| 248 |  |  |  |  |  |  | #path("/tmp/login.html")->spew_utf8($m->content); | 
| 249 |  |  |  |  |  |  |  | 
| 250 | 0 |  |  |  |  |  | $m->submit_form( | 
| 251 |  |  |  |  |  |  | with_fields => { | 
| 252 |  |  |  |  |  |  | 'login.email'    => $config->{email}, | 
| 253 |  |  |  |  |  |  | 'login.password' => $config->{password}, | 
| 254 |  |  |  |  |  |  | }, | 
| 255 |  |  |  |  |  |  | ); | 
| 256 |  |  |  |  |  |  | } # end login | 
| 257 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 258 |  |  |  |  |  |  |  | 
| 259 |  |  |  |  |  |  | sub scrape_response | 
| 260 |  |  |  |  |  |  | { | 
| 261 | 0 |  |  | 0 | 0 |  | my ($self, $response) = @_; | 
| 262 |  |  |  |  |  |  |  | 
| 263 | 0 |  |  |  |  |  | my $books = $self->scraper->scrape($response); | 
| 264 |  |  |  |  |  |  |  | 
| 265 | 0 |  |  |  |  |  | for my $book (@$books) { | 
| 266 | 0 |  | 0 |  |  |  | $book->{priority} //= 3; | 
| 267 | 0 |  |  |  |  |  | for ($book->{discount}) { | 
| 268 | 0 | 0 |  |  |  |  | next unless defined $_; | 
| 269 | 0 |  |  |  |  |  | s/^\s+//; | 
| 270 | 0 |  |  |  |  |  | s/\s+\z//; | 
| 271 | 0 |  |  |  |  |  | s/^\((.+)\)\z/$1/; | 
| 272 | 0 |  |  |  |  |  | s/^You save\s*//i; | 
| 273 |  |  |  |  |  |  | } | 
| 274 | 0 |  |  |  |  |  | $book->{date_added} =~ s!^(\d\d)/(\d\d)/(\d\d)$!20$3-$1-$2!; | 
| 275 |  |  |  |  |  |  | } | 
| 276 |  |  |  |  |  |  |  | 
| 277 | 0 |  |  |  |  |  | $books; | 
| 278 |  |  |  |  |  |  | } # end scrape_response | 
| 279 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 280 |  |  |  |  |  |  |  | 
| 281 |  |  |  |  |  |  | sub write_db | 
| 282 |  |  |  |  |  |  | { | 
| 283 | 0 |  |  | 0 | 0 |  | my ($self, $wishlist_url, $time_fetched, $books) = @_; | 
| 284 |  |  |  |  |  |  |  | 
| 285 | 0 |  |  |  |  |  | $time_fetched = _format_timestamp($time_fetched); | 
| 286 |  |  |  |  |  |  |  | 
| 287 | 0 |  |  |  |  |  | my $dbh = $self->dbh; | 
| 288 | 0 |  |  |  |  |  | my $updates = $self->updates; | 
| 289 |  |  |  |  |  |  |  | 
| 290 | 0 |  |  |  |  |  | my $wishlist_id = $self->get_wishlist_id($wishlist_url); | 
| 291 |  |  |  |  |  |  |  | 
| 292 | 0 |  |  |  |  |  | my $existing_priority = $self->get_existing_books($wishlist_id); | 
| 293 |  |  |  |  |  |  |  | 
| 294 | 0 |  |  |  |  |  | my $get_book = $dbh->prepare(<<''); | 
| 295 |  |  |  |  |  |  | SELECT title, author FROM books WHERE ean = ? | 
| 296 |  |  |  |  |  |  |  | 
| 297 | 0 |  |  |  |  |  | my $get_price = $dbh->prepare(<<''); | 
| 298 |  |  |  |  |  |  | SELECT price, list_price, discount, first_recorded FROM prices | 
| 299 |  |  |  |  |  |  | WHERE ean = ? AND current == 1 | 
| 300 |  |  |  |  |  |  |  | 
| 301 | 0 |  |  |  |  |  | for my $book (@$books) { | 
| 302 | 0 |  |  |  |  |  | my $ean = $book->{ean}; | 
| 303 | 0 |  |  |  |  |  | my $current_price_row; | 
| 304 |  |  |  |  |  |  |  | 
| 305 |  |  |  |  |  |  | # Update or add the book to the books table | 
| 306 | 0 |  |  |  |  |  | my $book_row = $dbh->selectrow_hashref($get_book, undef, $ean); | 
| 307 | 0 | 0 |  |  |  |  | if ($book_row) { | 
| 308 |  |  |  |  |  |  | # The book exists.  Update title & author if necessary | 
| 309 | 0 | 0 | 0 |  |  |  | unless (_eq($book_row->{title}, $book->{title}) and | 
| 310 |  |  |  |  |  |  | _eq($book_row->{author}, $book->{author})) { | 
| 311 | 0 |  |  |  |  |  | $dbh->do(<<'', undef, @$book{qw(title author ean)}); | 
| 312 |  |  |  |  |  |  | UPDATE books SET title = ?, author = ? WHERE ean = ? | 
| 313 |  |  |  |  |  |  |  | 
| 314 |  |  |  |  |  |  | } | 
| 315 |  |  |  |  |  |  | # Since the book exists, it might have a price | 
| 316 | 0 |  |  |  |  |  | $current_price_row = $dbh->selectrow_hashref($get_price, undef, $ean); | 
| 317 |  |  |  |  |  |  | } else { | 
| 318 |  |  |  |  |  |  | # The book doesn't exist; add it | 
| 319 | 0 |  |  |  |  |  | $dbh->do(<<'', undef, @$book{qw(title author ean)}); | 
| 320 |  |  |  |  |  |  | INSERT INTO books (title, author, ean) VALUES (?,?,?) | 
| 321 |  |  |  |  |  |  |  | 
| 322 |  |  |  |  |  |  | } | 
| 323 |  |  |  |  |  |  |  | 
| 324 |  |  |  |  |  |  | # Update or add the book to the wishlist_books table | 
| 325 | 0 | 0 |  |  |  |  | if (exists $existing_priority->{ $ean }) { | 
| 326 |  |  |  |  |  |  | # The book is already in the wishlist.  Update priority if necessary | 
| 327 | 0 | 0 |  |  |  |  | unless (_numEq($book->{priority}, $existing_priority->{ $ean })) { | 
| 328 | 0 |  |  |  |  |  | $dbh->do(<<'', undef, $book->{priority}, $wishlist_id, $ean); | 
| 329 |  |  |  |  |  |  | UPDATE wishlist_books SET priority = ? | 
| 330 |  |  |  |  |  |  | WHERE wishlist_id = ? AND ean = ? | 
| 331 |  |  |  |  |  |  |  | 
| 332 |  |  |  |  |  |  | } | 
| 333 |  |  |  |  |  |  | } else { | 
| 334 |  |  |  |  |  |  | # Add book to this wishlist | 
| 335 |  |  |  |  |  |  | ###   Inserting: @$book{qw(ean priority date_added)} | 
| 336 | 0 |  |  |  |  |  | $dbh->do(<<'', undef, $wishlist_id, @$book{qw(ean priority date_added)}); | 
| 337 |  |  |  |  |  |  | INSERT INTO wishlist_books (wishlist_id, ean, priority, date_added) | 
| 338 |  |  |  |  |  |  | VALUES (?,?,?,?) | 
| 339 |  |  |  |  |  |  |  | 
| 340 |  |  |  |  |  |  | } | 
| 341 |  |  |  |  |  |  |  | 
| 342 | 0 |  |  |  |  |  | for my $price (@$book{qw(price list_price)}) { | 
| 343 | 0 | 0 |  |  |  |  | next unless defined $price; | 
| 344 | 0 |  |  |  |  |  | $price =~ s/^\s*\$//; | 
| 345 | 0 |  |  |  |  |  | $price = int($price * 100 + 0.5); | 
| 346 |  |  |  |  |  |  | } | 
| 347 |  |  |  |  |  |  |  | 
| 348 | 1 |  |  | 1 |  | 1506 | { no warnings 'uninitialized';  $book->{discount} =~ s/\%// } | 
|  | 1 |  |  |  |  | 2 |  | 
|  | 1 |  |  |  |  | 2268 |  | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 349 |  |  |  |  |  |  |  | 
| 350 |  |  |  |  |  |  | # Update or add the prices entry | 
| 351 | 0 | 0 | 0 |  |  |  | if ($current_price_row and | 
|  |  |  | 0 |  |  |  |  | 
|  |  |  | 0 |  |  |  |  | 
| 352 |  |  |  |  |  |  | _numEq($current_price_row->{price}, $book->{price}) and | 
| 353 |  |  |  |  |  |  | _numEq($current_price_row->{list_price}, $book->{list_price}) and | 
| 354 |  |  |  |  |  |  | _numEq($current_price_row->{discount}, $book->{discount})) { | 
| 355 | 0 |  |  |  |  |  | $dbh->do(<<'', undef, $time_fetched, $ean, $current_price_row->{first_recorded}); | 
| 356 |  |  |  |  |  |  | UPDATE prices SET last_checked = ? | 
| 357 |  |  |  |  |  |  | WHERE ean = ? AND first_recorded = ? | 
| 358 |  |  |  |  |  |  |  | 
| 359 |  |  |  |  |  |  | } else { | 
| 360 | 0 | 0 |  |  |  |  | if ($current_price_row) { | 
| 361 | 0 |  |  |  |  |  | $dbh->do(<<'', undef, $ean, $current_price_row->{first_recorded}); | 
| 362 |  |  |  |  |  |  | UPDATE prices SET current = 0 WHERE ean = ? AND first_recorded = ? | 
| 363 |  |  |  |  |  |  |  | 
| 364 |  |  |  |  |  |  | } | 
| 365 | 0 |  |  |  |  |  | $updates->{$ean} = { | 
| 366 |  |  |  |  |  |  | old => $current_price_row, | 
| 367 |  |  |  |  |  |  | new => $book, | 
| 368 |  |  |  |  |  |  | }; | 
| 369 |  |  |  |  |  |  | ###   Inserting: $ean | 
| 370 | 0 |  |  |  |  |  | $dbh->do(<<'', undef, @$book{qw(ean price list_price discount)}, ($time_fetched)x2); | 
| 371 |  |  |  |  |  |  | INSERT INTO prices (ean, price, list_price, discount, first_recorded, last_checked) | 
| 372 |  |  |  |  |  |  | VALUES (?,?,?,?,?,?) | 
| 373 |  |  |  |  |  |  |  | 
| 374 |  |  |  |  |  |  | } | 
| 375 |  |  |  |  |  |  | } # end for each $book in @$books | 
| 376 |  |  |  |  |  |  |  | 
| 377 | 0 |  |  |  |  |  | $dbh->commit; | 
| 378 |  |  |  |  |  |  | } # end write_db | 
| 379 |  |  |  |  |  |  |  | 
| 380 |  |  |  |  |  |  | sub reduced_price_eans | 
| 381 |  |  |  |  |  |  | { | 
| 382 | 0 |  |  | 0 | 0 |  | my $updates = shift->updates; | 
| 383 |  |  |  |  |  |  |  | 
| 384 | 0 | 0 |  |  |  |  | sort { | 
| 385 | 0 |  |  |  |  |  | $updates->{$a}{new}{price} <=> $updates->{$b}{new}{price} or | 
| 386 |  |  |  |  |  |  | $updates->{$a}{new}{title} cmp $updates->{$b}{new}{title} | 
| 387 |  |  |  |  |  |  | } grep { | 
| 388 | 0 |  |  |  |  |  | my ($old, $new) = @{$updates->{$_}}{qw(old new)}; | 
|  | 0 |  |  |  |  |  |  | 
| 389 | 0 | 0 | 0 |  |  |  | $old and defined($new->{price}) | 
|  |  |  | 0 |  |  |  |  | 
| 390 |  |  |  |  |  |  | and (!defined($old->{price}) or $new->{price} < $old->{price}); | 
| 391 |  |  |  |  |  |  | } keys %$updates; | 
| 392 |  |  |  |  |  |  | } # end reduced_price_eans | 
| 393 |  |  |  |  |  |  |  | 
| 394 |  |  |  |  |  |  | sub get_existing_books | 
| 395 |  |  |  |  |  |  | { | 
| 396 | 0 |  |  | 0 | 0 |  | my ($self, $wishlist_id) = @_; | 
| 397 |  |  |  |  |  |  |  | 
| 398 | 0 |  |  |  |  |  | my %existing_priority; | 
| 399 |  |  |  |  |  |  |  | 
| 400 | 0 |  |  |  |  |  | my $s = $self->dbh->prepare(<<''); | 
| 401 |  |  |  |  |  |  | SELECT ean, priority FROM wishlist_books | 
| 402 |  |  |  |  |  |  | WHERE wishlist_id = ? AND date_removed IS NULL | 
| 403 |  |  |  |  |  |  |  | 
| 404 | 0 |  |  |  |  |  | $s->execute($wishlist_id); | 
| 405 | 0 |  |  |  |  |  | $s->bind_columns( \( my ($ean, $priority) ) ); | 
| 406 | 0 |  |  |  |  |  | while ($s->fetch) { | 
| 407 | 0 |  |  |  |  |  | $existing_priority{$ean} = $priority; | 
| 408 |  |  |  |  |  |  | } | 
| 409 |  |  |  |  |  |  |  | 
| 410 | 0 |  |  |  |  |  | \%existing_priority; | 
| 411 |  |  |  |  |  |  | } # end get_existing_books | 
| 412 |  |  |  |  |  |  |  | 
| 413 |  |  |  |  |  |  | sub get_wishlist_id | 
| 414 |  |  |  |  |  |  | { | 
| 415 | 0 |  |  | 0 | 0 |  | my ($self, $wishlist_url) = @_; | 
| 416 |  |  |  |  |  |  |  | 
| 417 | 0 |  |  |  |  |  | my $dbh = $self->dbh; | 
| 418 |  |  |  |  |  |  |  | 
| 419 | 0 |  |  |  |  |  | my ($wishlist_id) = $dbh->selectrow_array(<<'', undef, $wishlist_url); | 
| 420 |  |  |  |  |  |  | SELECT wishlist_id FROM wishlists WHERE url = ? | 
| 421 |  |  |  |  |  |  |  | 
| 422 | 0 | 0 |  |  |  |  | unless (defined $wishlist_id) { | 
| 423 | 0 |  |  |  |  |  | $dbh->do(<<'', undef, $wishlist_url); | 
| 424 |  |  |  |  |  |  | INSERT INTO wishlists (url) VALUES (?) | 
| 425 |  |  |  |  |  |  |  | 
| 426 | 0 |  | 0 |  |  |  | $wishlist_id = $dbh->last_insert_id((undef)x4) | 
| 427 |  |  |  |  |  |  | // die "Unable to insert wishlist $wishlist_url"; | 
| 428 |  |  |  |  |  |  | } | 
| 429 |  |  |  |  |  |  |  | 
| 430 | 0 |  |  |  |  |  | $wishlist_id; | 
| 431 |  |  |  |  |  |  | } # end get_wishlist_id | 
| 432 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 433 |  |  |  |  |  |  |  | 
| 434 |  |  |  |  |  |  | sub describe_selected_updates | 
| 435 |  |  |  |  |  |  | { | 
| 436 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 437 |  |  |  |  |  |  |  | 
| 438 | 0 |  |  |  |  |  | my $updates = $self->updates; | 
| 439 |  |  |  |  |  |  |  | 
| 440 | 0 |  |  |  |  |  | map { | 
| 441 | 0 |  |  |  |  |  | my $book = $updates->{$_}{new}; | 
| 442 | 0 |  |  |  |  |  | my $price = _format_price($book->{price}); | 
| 443 | 0 | 0 |  |  |  |  | if (my $old = $updates->{$_}{old}) { | 
| 444 | 0 |  |  |  |  |  | $price .= sprintf ' (was %s)', _format_price($old->{price}); | 
| 445 |  |  |  |  |  |  | } | 
| 446 | 0 |  |  |  |  |  | <<"END UPDATE"; | 
| 447 |  |  |  |  |  |  | Title:  $book->{title}  ($_) | 
| 448 |  |  |  |  |  |  | Author: $book->{author} | 
| 449 |  |  |  |  |  |  | Price:  $price | 
| 450 |  |  |  |  |  |  | END UPDATE | 
| 451 |  |  |  |  |  |  | } @_; | 
| 452 |  |  |  |  |  |  | } # end describe_selected_updates | 
| 453 |  |  |  |  |  |  |  | 
| 454 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 455 |  |  |  |  |  |  |  | 
| 456 |  |  |  |  |  |  | sub email_price_drop_alert | 
| 457 |  |  |  |  |  |  | { | 
| 458 | 0 |  |  | 0 | 0 |  | my ($self) = @_; | 
| 459 |  |  |  |  |  |  |  | 
| 460 | 0 | 0 |  |  |  |  | my @eans = $self->reduced_price_eans or return; | 
| 461 |  |  |  |  |  |  |  | 
| 462 | 0 |  |  |  |  |  | require Email::Sender::Simple; | 
| 463 | 0 |  |  |  |  |  | require Email::Simple; | 
| 464 | 0 |  |  |  |  |  | require Email::Simple::Creator; | 
| 465 | 0 |  |  |  |  |  | require Encode; | 
| 466 |  |  |  |  |  |  |  | 
| 467 | 0 |  |  |  |  |  | my $updates = $self->updates; | 
| 468 | 0 |  |  |  |  |  | my $config  = $self->config->{_}; | 
| 469 |  |  |  |  |  |  |  | 
| 470 | 0 |  | 0 |  |  |  | my $address = $config->{report} || $config->{email}; | 
| 471 | 0 |  |  |  |  |  | my @body = $self->describe_selected_updates(@eans); | 
| 472 |  |  |  |  |  |  |  | 
| 473 | 0 |  |  |  |  |  | my $subject = (@eans > 2) | 
| 474 |  |  |  |  |  |  | ? sprintf('%d books', scalar @eans) | 
| 475 |  |  |  |  |  |  | : Encode::encode('MIME-Header', | 
| 476 | 0 | 0 |  |  |  |  | join(' & ', map { $updates->{$_}{new}{title} } @eans) | 
| 477 |  |  |  |  |  |  | ); | 
| 478 |  |  |  |  |  |  |  | 
| 479 | 0 |  |  |  |  |  | my $email = Email::Simple->create( | 
| 480 |  |  |  |  |  |  | header => [ | 
| 481 |  |  |  |  |  |  | To      => $address, | 
| 482 |  |  |  |  |  |  | From    => qq'"Barnes & Noble Wishlist" <$address>', | 
| 483 |  |  |  |  |  |  | Subject => "Price Drop Alert: $subject", | 
| 484 |  |  |  |  |  |  | 'MIME-Version' => '1.0', | 
| 485 |  |  |  |  |  |  | 'Content-Type' => 'text/plain; charset=UTF-8', | 
| 486 |  |  |  |  |  |  | 'Content-Transfer-Encoding' => '8bit', | 
| 487 |  |  |  |  |  |  | ], | 
| 488 |  |  |  |  |  |  | body => Encode::encode('utf8', join("\n", @body)), | 
| 489 |  |  |  |  |  |  | ); | 
| 490 |  |  |  |  |  |  |  | 
| 491 | 0 |  |  |  |  |  | Email::Sender::Simple->send($email); | 
| 492 |  |  |  |  |  |  | } # end email_price_drop_alert | 
| 493 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 494 |  |  |  |  |  |  |  | 
| 495 |  |  |  |  |  |  | sub print_matching_books | 
| 496 |  |  |  |  |  |  | { | 
| 497 | 0 |  |  | 0 | 0 |  | my ($self, $search) = @_; | 
| 498 |  |  |  |  |  |  |  | 
| 499 | 0 |  |  |  |  |  | my $books = $self->dbh->selectall_arrayref(<<'END SEARCH', undef, ("%$search%")x2); | 
| 500 |  |  |  |  |  |  | SELECT ean, price, title, author FROM books NATURAL JOIN prices | 
| 501 |  |  |  |  |  |  | WHERE prices.current AND (title LIKE ? OR author LIKE ?) | 
| 502 |  |  |  |  |  |  | ORDER by title, author | 
| 503 |  |  |  |  |  |  | END SEARCH | 
| 504 |  |  |  |  |  |  |  | 
| 505 | 0 | 0 |  |  |  |  | if (@$books == 1) { | 
| 506 | 0 |  |  |  |  |  | print "$books->[0][0] "; | 
| 507 | 0 |  |  |  |  |  | $self->print_price_history($books->[0][0]); | 
| 508 |  |  |  |  |  |  | } else { | 
| 509 | 0 |  |  |  |  |  | foreach my $row (@$books) { | 
| 510 | 0 |  |  |  |  |  | $row->[1] = _format_price($row->[1]); | 
| 511 | 0 |  |  |  |  |  | printf "%s %6s %s by %s\n", @$row; | 
| 512 |  |  |  |  |  |  | } | 
| 513 | 0 |  |  |  |  |  | print "\n"; | 
| 514 |  |  |  |  |  |  | } | 
| 515 |  |  |  |  |  |  | } # end print_matching_books | 
| 516 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 517 |  |  |  |  |  |  |  | 
| 518 |  |  |  |  |  |  | sub print_updates_since | 
| 519 |  |  |  |  |  |  | { | 
| 520 | 0 |  |  | 0 | 0 |  | my ($self, $since_date) = @_; | 
| 521 |  |  |  |  |  |  |  | 
| 522 | 0 |  |  |  |  |  | my $s = $self->dbh->prepare(<<'END SEARCH'); | 
| 523 |  |  |  |  |  |  | SELECT ean, price, title, author FROM books NATURAL JOIN prices | 
| 524 |  |  |  |  |  |  | WHERE prices.current AND first_recorded >= ? | 
| 525 |  |  |  |  |  |  | ORDER by first_recorded, price, title, author | 
| 526 |  |  |  |  |  |  | END SEARCH | 
| 527 |  |  |  |  |  |  |  | 
| 528 | 0 |  |  |  |  |  | $s->execute($since_date); | 
| 529 |  |  |  |  |  |  |  | 
| 530 | 0 |  |  |  |  |  | while (my $row = $s->fetch) { | 
| 531 | 0 |  |  |  |  |  | $row->[1] = _format_price($row->[1]); | 
| 532 | 0 |  |  |  |  |  | printf "%s %6s %s by %s\n", @$row; | 
| 533 |  |  |  |  |  |  | } | 
| 534 |  |  |  |  |  |  |  | 
| 535 | 0 |  |  |  |  |  | print "\n"; | 
| 536 |  |  |  |  |  |  | } # end print_updates_since | 
| 537 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 538 |  |  |  |  |  |  |  | 
| 539 |  |  |  |  |  |  | sub print_price_history | 
| 540 |  |  |  |  |  |  | { | 
| 541 | 0 |  |  | 0 | 0 |  | my ($self, $ean) = @_; | 
| 542 |  |  |  |  |  |  |  | 
| 543 | 0 |  |  |  |  |  | my $dbh = $self->dbh; | 
| 544 |  |  |  |  |  |  |  | 
| 545 | 0 |  |  |  |  |  | my $book = $dbh->selectrow_hashref( | 
| 546 |  |  |  |  |  |  | 'SELECT title, author FROM books WHERE ean = ?', undef, $ean | 
| 547 |  |  |  |  |  |  | ); | 
| 548 |  |  |  |  |  |  |  | 
| 549 | 0 |  |  |  |  |  | print "$book->{title} by $book->{author}\n"; | 
| 550 |  |  |  |  |  |  |  | 
| 551 | 0 |  |  |  |  |  | my $history = $dbh->prepare(<<'END HISTORY'); | 
| 552 |  |  |  |  |  |  | SELECT first_recorded, last_checked, price, list_price, discount | 
| 553 |  |  |  |  |  |  | FROM prices WHERE ean = ? ORDER BY first_recorded | 
| 554 |  |  |  |  |  |  | END HISTORY | 
| 555 |  |  |  |  |  |  |  | 
| 556 | 0 |  |  |  |  |  | $history->execute($ean); | 
| 557 |  |  |  |  |  |  |  | 
| 558 | 0 |  |  |  |  |  | while (my $row = $history->fetchrow_hashref) { | 
| 559 | 0 |  |  |  |  |  | $_ =~ s/ .+// for @$row{qw(first_recorded last_checked)}; | 
| 560 | 0 | 0 |  |  |  |  | printf("%s - %s %6s%s%s\n", @$row{qw(first_recorded last_checked)}, | 
|  |  | 0 |  |  |  |  |  | 
| 561 |  |  |  |  |  |  | _format_price($row->{price}), | 
| 562 |  |  |  |  |  |  | $row->{list_price} | 
| 563 |  |  |  |  |  |  | ? " (list " . _format_price($row->{list_price}) . ")" | 
| 564 |  |  |  |  |  |  | : '', | 
| 565 |  |  |  |  |  |  | $row->{discount} ? " ($row->{discount}% off)" : ''); | 
| 566 |  |  |  |  |  |  | } | 
| 567 |  |  |  |  |  |  |  | 
| 568 | 0 |  |  |  |  |  | print "\n"; | 
| 569 |  |  |  |  |  |  | } # end print_price_history | 
| 570 |  |  |  |  |  |  |  | 
| 571 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 572 |  |  |  |  |  |  |  | 
| 573 |  |  |  |  |  |  | sub print_updates | 
| 574 |  |  |  |  |  |  | { | 
| 575 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 576 |  |  |  |  |  |  |  | 
| 577 | 0 |  |  |  |  |  | my $updates = $self->updates; | 
| 578 |  |  |  |  |  |  |  | 
| 579 | 0 | 0 |  |  |  |  | my @eans = sort { | 
| 580 | 0 |  |  |  |  |  | $updates->{$a}{new}{title}  cmp $updates->{$b}{new}{title} or | 
| 581 |  |  |  |  |  |  | $updates->{$a}{new}{author} cmp $updates->{$b}{new}{author} | 
| 582 |  |  |  |  |  |  | } keys %$updates; | 
| 583 |  |  |  |  |  |  |  | 
| 584 | 0 |  |  |  |  |  | print join("\n", $self->describe_selected_updates(@eans)); | 
| 585 |  |  |  |  |  |  | } # end print_updates | 
| 586 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 587 |  |  |  |  |  |  |  | 
| 588 |  |  |  |  |  |  | sub update_wishlists | 
| 589 |  |  |  |  |  |  | { | 
| 590 | 0 |  |  | 0 | 0 |  | my $self = shift; | 
| 591 |  |  |  |  |  |  |  | 
| 592 | 0 |  |  |  |  |  | my $config  = $self->config; | 
| 593 | 0 |  |  |  |  |  | my $m       = $self->mech; | 
| 594 |  |  |  |  |  |  |  | 
| 595 |  |  |  |  |  |  | # Ensure we can open the database before we start making web requests | 
| 596 | 0 |  |  |  |  |  | $self->dbh; | 
| 597 |  |  |  |  |  |  |  | 
| 598 | 0 |  |  |  |  |  | $self->login; | 
| 599 |  |  |  |  |  |  |  | 
| 600 | 0 |  |  |  |  |  | for my $wishlist (sort keys %$config) { | 
| 601 | 0 | 0 |  |  |  |  | next if $wishlist eq '_';   # the root INI section | 
| 602 |  |  |  |  |  |  |  | 
| 603 | 0 |  |  |  |  |  | my $response = $m->get( $config->{$wishlist}{wishlist} ); | 
| 604 | 0 |  |  |  |  |  | my $books    = $self->scrape_response($response); | 
| 605 |  |  |  |  |  |  | #    path("/tmp/wishlist.html")->spew_utf8($response->content); | 
| 606 | 0 |  | 0 |  |  |  | $self->write_db($config->{$wishlist}{wishlist}, $response->last_modified // $response->date, $books); | 
| 607 |  |  |  |  |  |  | } | 
| 608 |  |  |  |  |  |  | } # end update_wishlists | 
| 609 |  |  |  |  |  |  |  | 
| 610 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 611 |  |  |  |  |  |  |  | 
| 612 |  |  |  |  |  |  | sub usage { | 
| 613 | 0 |  |  | 0 | 0 |  | my $name = $0; | 
| 614 | 0 |  |  |  |  |  | $name =~ s!^.*[/\\]!!; | 
| 615 |  |  |  |  |  |  |  | 
| 616 | 0 |  |  |  |  |  | shift->close_dbh; | 
| 617 |  |  |  |  |  |  |  | 
| 618 | 0 |  |  |  |  |  | print "$name $VERSION\n"; | 
| 619 | 0 | 0 | 0 |  |  |  | exit if $_[0] and $_[0] eq 'version'; | 
| 620 | 0 |  |  |  |  |  | print <<"END USAGE"; | 
| 621 |  |  |  |  |  |  | \nUsage:  $name [options] [EAN_or_TITLE_or_AUTHOR] ... | 
| 622 |  |  |  |  |  |  | -e, --email              Send Price Drop Alert email (implies --update) | 
| 623 |  |  |  |  |  |  | -q, --quiet              Don't print list of updates | 
| 624 |  |  |  |  |  |  | -s, --since=DATE         Print books whose price changed on or after DATE | 
| 625 |  |  |  |  |  |  | -u, --update             Download current prices from wishlist | 
| 626 |  |  |  |  |  |  | --configure          Create and/or edit the config file | 
| 627 |  |  |  |  |  |  | --help               Display this help message | 
| 628 |  |  |  |  |  |  | --version            Display version information | 
| 629 |  |  |  |  |  |  | END USAGE | 
| 630 |  |  |  |  |  |  |  | 
| 631 | 0 |  |  |  |  |  | exit; | 
| 632 |  |  |  |  |  |  | } # end usage | 
| 633 |  |  |  |  |  |  | #--------------------------------------------------------------------- | 
| 634 |  |  |  |  |  |  |  | 
| 635 |  |  |  |  |  |  | sub run | 
| 636 |  |  |  |  |  |  | { | 
| 637 | 0 |  |  | 0 | 0 |  | my ($self, @args) = @_; | 
| 638 |  |  |  |  |  |  |  | 
| 639 |  |  |  |  |  |  | # Process command line options | 
| 640 | 0 |  |  |  |  |  | my ($fetch_wishlist, $quiet, $send_email, $since_date); | 
| 641 |  |  |  |  |  |  | { | 
| 642 | 0 |  |  |  |  |  | require Getopt::Long; Getopt::Long->VERSION(2.24); # object-oriented | 
|  | 0 |  |  |  |  |  |  | 
|  | 0 |  |  |  |  |  |  | 
| 643 | 0 |  |  |  |  |  | my $getopt = Getopt::Long::Parser->new( | 
| 644 |  |  |  |  |  |  | config => [qw(bundling no_getopt_compat)] | 
| 645 |  |  |  |  |  |  | ); | 
| 646 | 0 |  |  | 0 |  |  | my $usage = sub { $self->usage(@_) }; | 
|  | 0 |  |  |  |  |  |  | 
| 647 |  |  |  |  |  |  |  | 
| 648 |  |  |  |  |  |  | $getopt->getoptionsfromarray(\@args, | 
| 649 |  |  |  |  |  |  | 'email|e'   => \$send_email, | 
| 650 |  |  |  |  |  |  | 'quiet|q'   => \$quiet, | 
| 651 |  |  |  |  |  |  | 'since|s=s' => \$since_date, | 
| 652 |  |  |  |  |  |  | 'update|u'  => \$fetch_wishlist, | 
| 653 | 0 |  |  | 0 |  |  | 'configure' => sub { $self->configure; exit }, | 
|  | 0 |  |  |  |  |  |  | 
| 654 | 0 | 0 |  |  |  |  | 'help'      => $usage, | 
| 655 |  |  |  |  |  |  | 'version'   => $usage | 
| 656 |  |  |  |  |  |  | ) or $self->usage; | 
| 657 |  |  |  |  |  |  | } | 
| 658 |  |  |  |  |  |  |  | 
| 659 |  |  |  |  |  |  | # Update database & send email if requested | 
| 660 | 0 | 0 | 0 |  |  |  | if ($fetch_wishlist or $send_email) { | 
|  |  | 0 | 0 |  |  |  |  | 
| 661 | 0 |  |  |  |  |  | $self->update_wishlists; | 
| 662 |  |  |  |  |  |  |  | 
| 663 | 0 | 0 |  |  |  |  | $self->email_price_drop_alert if $send_email; | 
| 664 | 0 | 0 |  |  |  |  | $self->print_updates unless $quiet; | 
| 665 |  |  |  |  |  |  | } elsif (not @args and not $since_date) { | 
| 666 |  |  |  |  |  |  | # Didn't fetch updates and no request to display book data | 
| 667 | 0 | 0 |  |  |  |  | if ($self->config_file->is_file) { | 
| 668 | 0 |  |  |  |  |  | $self->usage; | 
| 669 |  |  |  |  |  |  | } else { | 
| 670 | 0 |  |  |  |  |  | $self->configure; | 
| 671 |  |  |  |  |  |  | } | 
| 672 |  |  |  |  |  |  | } | 
| 673 |  |  |  |  |  |  |  | 
| 674 | 0 | 0 |  |  |  |  | if ($since_date) { | 
| 675 | 0 |  |  |  |  |  | $self->print_updates_since($since_date); | 
| 676 |  |  |  |  |  |  | } | 
| 677 |  |  |  |  |  |  |  | 
| 678 |  |  |  |  |  |  | # Display data from the database about requested books | 
| 679 | 0 |  |  |  |  |  | foreach my $arg (@args) { | 
| 680 | 0 | 0 |  |  |  |  | if ($arg =~ /^[0-9]{13}\z/) { | 
| 681 | 0 |  |  |  |  |  | $self->print_price_history($arg); | 
| 682 |  |  |  |  |  |  | } else { | 
| 683 | 0 |  |  |  |  |  | $self->print_matching_books($arg); | 
| 684 |  |  |  |  |  |  | } | 
| 685 |  |  |  |  |  |  | } | 
| 686 |  |  |  |  |  |  |  | 
| 687 |  |  |  |  |  |  | # Disconnect from the database | 
| 688 | 0 |  |  |  |  |  | $self->close_dbh; | 
| 689 |  |  |  |  |  |  | } # end run | 
| 690 |  |  |  |  |  |  |  | 
| 691 |  |  |  |  |  |  | #===================================================================== | 
| 692 |  |  |  |  |  |  | # Package Return Value: | 
| 693 |  |  |  |  |  |  |  | 
| 694 |  |  |  |  |  |  | 1; | 
| 695 |  |  |  |  |  |  |  | 
| 696 |  |  |  |  |  |  | __END__ |