File Coverage

blib/lib/App/BarnesNoble/WishListMinder.pm
Criterion Covered Total %
statement 21 257 8.1
branch 0 84 0.0
condition 0 59 0.0
subroutine 7 41 17.0
pod 0 19 0.0
total 28 460 6.0


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   15856 use 5.010;
  1         3  
  1         36  
21 1     1   4 use strict;
  1         1  
  1         29  
22 1     1   4 use warnings;
  1         1  
  1         35  
23              
24             our $VERSION = '0.004';
25             # This file is part of App-BarnesNoble-WishListMinder 0.004 (December 20, 2014)
26              
27 1     1   649 use Path::Tiny;
  1         12619  
  1         62  
28             #use Smart::Comments;
29              
30 1     1   486 use Moo;
  1         11313  
  1         5  
31 1     1   1648 use namespace::clean;
  1         9204  
  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   1481 { no warnings 'uninitialized'; $book->{discount} =~ s/\%// }
  1         1  
  1         1918  
  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, $all_history) = @_;
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 0       if ($all_history or @$books == 1) {
506 0           foreach my $row (@$books) {
507 0           print "$row->[0] ";
508 0           $self->print_price_history($row->[0]);
509             }
510             } else {
511 0           foreach my $row (@$books) {
512 0           $row->[1] = _format_price($row->[1]);
513 0           printf "%s %6s %s by %s\n", @$row;
514             }
515 0           print "\n";
516             }
517             } # end print_matching_books
518             #---------------------------------------------------------------------
519              
520             sub print_updates_since
521             {
522 0     0 0   my ($self, $since_date) = @_;
523              
524 0           my $s = $self->dbh->prepare(<<'END SEARCH');
525             SELECT ean, price, title, author FROM books NATURAL JOIN prices
526             WHERE prices.current AND first_recorded >= ?
527             ORDER by first_recorded, price, title, author
528             END SEARCH
529              
530 0           $s->execute($since_date);
531              
532 0           while (my $row = $s->fetch) {
533 0           $row->[1] = _format_price($row->[1]);
534 0           printf "%s %6s %s by %s\n", @$row;
535             }
536              
537 0           print "\n";
538             } # end print_updates_since
539             #---------------------------------------------------------------------
540              
541             sub print_price_history
542             {
543 0     0 0   my ($self, $ean) = @_;
544              
545 0           my $dbh = $self->dbh;
546              
547 0           my $book = $dbh->selectrow_hashref(
548             'SELECT title, author FROM books WHERE ean = ?', undef, $ean
549             );
550              
551 0           print "$book->{title} by $book->{author}\n";
552              
553 0           my $history = $dbh->prepare(<<'END HISTORY');
554             SELECT first_recorded, last_checked, price, list_price, discount
555             FROM prices WHERE ean = ? ORDER BY first_recorded
556             END HISTORY
557              
558 0           $history->execute($ean);
559              
560 0           while (my $row = $history->fetchrow_hashref) {
561 0           $_ =~ s/ .+// for @$row{qw(first_recorded last_checked)};
562 0 0         printf("%s - %s %6s%s%s\n", @$row{qw(first_recorded last_checked)},
    0          
563             _format_price($row->{price}),
564             $row->{list_price}
565             ? " (list " . _format_price($row->{list_price}) . ")"
566             : '',
567             $row->{discount} ? " ($row->{discount}% off)" : '');
568             }
569              
570 0           print "\n";
571             } # end print_price_history
572              
573             #---------------------------------------------------------------------
574              
575             sub print_updates
576             {
577 0     0 0   my $self = shift;
578              
579 0           my $updates = $self->updates;
580              
581 0 0         my @eans = sort {
582 0           $updates->{$a}{new}{title} cmp $updates->{$b}{new}{title} or
583             $updates->{$a}{new}{author} cmp $updates->{$b}{new}{author}
584             } keys %$updates;
585              
586 0           print join("\n", $self->describe_selected_updates(@eans));
587             } # end print_updates
588             #---------------------------------------------------------------------
589              
590             sub have_user_cookie
591             {
592 0     0 0   my $have_cookie;
593 0           my $min_expires = time() + 30;
594              
595             shift->mech->cookie_jar->scan(sub {
596 0 0 0 0     $have_cookie = 1 if $_[1] eq 'userid'
      0        
597             and $_[4] eq '.barnesandnoble.com'
598             and $_[8] > $min_expires
599 0           });
600              
601 0           $have_cookie;
602             } # end have_user_cookie
603              
604             #---------------------------------------------------------------------
605              
606             sub update_wishlists
607             {
608 0     0 0   my $self = shift;
609              
610 0           my $config = $self->config;
611 0           my $m = $self->mech;
612              
613             # Ensure we can open the database before we start making web requests
614 0           $self->dbh;
615              
616 0 0         $self->login unless $self->have_user_cookie;
617              
618 0           for my $wishlist (sort keys %$config) {
619 0 0         next if $wishlist eq '_'; # the root INI section
620              
621 0           my $response = $m->get( $config->{$wishlist}{wishlist} );
622 0           my $books = $self->scrape_response($response);
623 0 0         unless (@$books) {
624 0           warn "$config->{$wishlist}{wishlist} has no entries\n";
625             # Save the response for debugging:
626 0           $self->dir->child("empty-$wishlist.html")->spew_utf8($response->content);
627             }
628             # path("/tmp/wishlist.html")->spew_utf8($response->content);
629 0   0       $self->write_db($config->{$wishlist}{wishlist}, $response->last_modified // $response->date, $books);
630             }
631             } # end update_wishlists
632              
633             #---------------------------------------------------------------------
634              
635             sub usage {
636 0     0 0   my $name = $0;
637 0           $name =~ s!^.*[/\\]!!;
638              
639 0           shift->close_dbh;
640              
641 0           print "$name $VERSION\n";
642 0 0 0       exit if $_[0] and $_[0] eq 'version';
643 0           print <<"END USAGE";
644             \nUsage: $name [options] [EAN_or_TITLE_or_AUTHOR] ...
645             -a, --all-history Show price history even when multiple items match
646             -e, --email Send Price Drop Alert email (implies --update)
647             -q, --quiet Don't print list of updates
648             -s, --since=DATE Print books whose price changed on or after DATE
649             -u, --update Download current prices from wishlist
650             --configure Create and/or edit the config file
651             --help Display this help message
652             --version Display version information
653             END USAGE
654              
655 0           exit;
656             } # end usage
657             #---------------------------------------------------------------------
658              
659             sub run
660             {
661 0     0 0   my ($self, @args) = @_;
662              
663             # Process command line options
664 0           my ($all_history, $fetch_wishlist, $quiet, $send_email, $since_date);
665             {
666 0           require Getopt::Long; Getopt::Long->VERSION(2.24); # object-oriented
  0            
  0            
667 0           my $getopt = Getopt::Long::Parser->new(
668             config => [qw(bundling no_getopt_compat)]
669             );
670 0     0     my $usage = sub { $self->usage(@_) };
  0            
671              
672             $getopt->getoptionsfromarray(\@args,
673             'all-history|a' => \$all_history,
674             'email|e' => \$send_email,
675             'quiet|q' => \$quiet,
676             'since|s=s' => \$since_date,
677             'update|u' => \$fetch_wishlist,
678 0     0     'configure' => sub { $self->configure; exit },
  0            
679 0 0         'help' => $usage,
680             'version' => $usage
681             ) or $self->usage;
682             }
683              
684             # Update database & send email if requested
685 0 0 0       if ($fetch_wishlist or $send_email) {
    0 0        
686 0           $self->update_wishlists;
687              
688 0 0         $self->email_price_drop_alert if $send_email;
689 0 0         $self->print_updates unless $quiet;
690             } elsif (not @args and not $since_date) {
691             # Didn't fetch updates and no request to display book data
692 0 0         if ($self->config_file->is_file) {
693 0           $self->usage;
694             } else {
695 0           $self->configure;
696             }
697             }
698              
699 0 0         if ($since_date) {
700 0           $self->print_updates_since($since_date);
701             }
702              
703             # Display data from the database about requested books
704 0           foreach my $arg (@args) {
705 0 0         if ($arg =~ /^[0-9]{13}\z/) {
706 0           $self->print_price_history($arg);
707             } else {
708 0           $self->print_matching_books($arg, $all_history);
709             }
710             }
711              
712             # Disconnect from the database
713 0           $self->close_dbh;
714             } # end run
715              
716             #=====================================================================
717             # Package Return Value:
718              
719             1;
720              
721             __END__